seekrit
Docs/Kubernetes (ESO)

Kubernetes (External Secrets Operator)

There are two ways to get seekrit secrets into a Kubernetes workload:

  1. Inject at runtime — run your process through seekrit run, which resolves and decrypts into the process environment. Nothing is written to the cluster. This is the CI/CD guide's Kubernetes pattern.
  2. Sync into a Kubernetes Secret — declaratively, with the External Secrets Operator (ESO), so your pods consume a normal Secret via envFrom/valueFrom and nothing in them knows about seekrit. That's this guide.

How it works

seekrit is zero-knowledge: GET /v1/resolve returns only ciphertext plus a data key wrapped to your token's public key, and decryption happens client-side. ESO, which expects to pull plaintext from a provider, therefore can't read from the seekrit API directly.

The seekrit-eso Helm chart bridges that gap. It deploys a small in-cluster sidecar (seekrit-sdk-server) that holds a service token, resolves and decrypts locally, caches the result, and serves it over a tiny authed HTTP API — then generates a webhook SecretStore that points ESO at the sidecar. You run stock, unmodified ESO and write only ExternalSecret resources.

ExternalSecret ─▶ ESO (stock) ──webhook──▶ seekrit-sdk-server ──/v1/resolve──▶ seekrit API
 (you write)      (unmodified)             (the chart)                         (ciphertext only)
note

The seekrit API still never sees plaintext — decryption stays inside your cluster, exactly as it does for the CLI and seekrit-run. The sidecar is simply "another principal" holding a grant.

Prerequisites

1. External Secrets Operator, installed once per cluster:

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

2. A seekrit service token bound to the app environment you want to sync. Mint one with the CLI (see Service tokens):

seekrit token create --name eso --app storefront --env production
# prints:  skt_XXXXXXXX_…    (save it now)

The token is bound to that one environment and auto-granted its keys (plus any composed group slices), so it can read exactly that environment and nothing else.

Install the chart

helm install seekrit-eso oci://registry-1.docker.io/seekritdev/seekrit-eso \
  --version 0.2.0 \
  -n seekrit-system --create-namespace \
  --set seekrit.token=skt_XXXXXXXX_…

That deploys the sidecar and creates a SecretStore named seekrit. The chart also generates the sidecar's API key automatically and preserves it across helm upgrade.

tip

Managing secrets with GitOps? Don't put the token in --set. Create a Secret yourself (sealed-secrets, SOPS, etc.) and point the chart at it with --set seekrit.existingSecret=my-token-secret.

Write an ExternalSecret

Now the payoff — the only thing your team writes per app:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: storefront
  namespace: seekrit-system
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: seekrit
    kind: SecretStore
  target:
    name: storefront-secrets      # the Kubernetes Secret ESO creates/manages
  data:
    - secretKey: DATABASE_URL      # key in the resulting Secret
      remoteRef:
        key: DATABASE_URL          # secret name in your seekrit environment
    - secretKey: STRIPE_API_KEY
      remoteRef:
        key: STRIPE_API_KEY
note

The generated SecretStore is namespaced (secretStore.kind defaults to SecretStore), so any ExternalSecret referencing it must live in the same namespace you installed the chart intoseekrit-system above. To reference it from other namespaces (e.g. your app's own namespace) instead, install with --set secretStore.kind=ClusterSecretStore.

ESO creates a storefront-secrets Secret in that namespace and keeps it in sync. Consume it like any other Secret (from a pod in the same namespace):

envFrom:
  - secretRef:
      name: storefront-secrets

Check status:

kubectl -n seekrit-system get externalsecret storefront
kubectl -n seekrit-system logs deploy/seekrit-eso

Secret references

${OTHER_SECRET} references are expanded by the sidecar, over the merged environment, before ESO ever sees a value — so the Secret in the cluster holds the assembled string.

Pulling a whole environment

ESO's webhook provider fetches one key at a time, so list the keys you want in data[] (the sidecar caches the decrypted environment, so each fetch is cheap). To pull everything without enumerating keys, template against the sidecar's whole-environment map with dataFrom + target.template — the sidecar exposes GET /v1/secrets returning {"data": {NAME: value, …}}.

note

A native seekrit ESO provider (first-class provider: {seekrit:} with built-in dataFrom support) is planned. It reuses this same sidecar, so when it lands the chart swaps its generated SecretStore with no change to your ExternalSecrets.

Rotation & refresh timing

Two intervals stack. The sidecar re-resolves from seekrit every refreshInterval (chart value, default 60s), and ESO re-reads the sidecar every spec.refreshInterval on the ExternalSecret. So a rotated secret reaches your pods within roughly the sum of the two. Lower either for faster propagation.

Surviving a seekrit outage

The sidecar's first resolve is fail-closed: if the seekrit API is unreachable when it starts, it refuses to bind. That is the right default for a component that hands out plaintext — but it means a pod rescheduled during an outage takes every ExternalSecret it backs down with it. (Once running, it is already resilient: a failed refresh keeps serving the last-good snapshot in memory.)

Turn on the last-known-good cache to close that gap:

cache:
  enabled: true
  maxAge: 24h
  # emptyDir survives a container restart but not a reschedule.
  # Use a PVC to survive both:
  # volume:
  #   persistentVolumeClaim:
  #     claimName: seekrit-lkg-cache

The sidecar then writes each successful resolve — the encrypted response only — to the volume, and a restart that cannot reach the API starts from that copy instead of failing. It keeps retrying on a short backoff and switches to live secrets as soon as the API answers.

Decrypting the cached copy still requires the service token, so the volume is no more sensitive than the token Secret already mounted into the pod. The trade-off is the one described in the CLI reference: while the API is unreachable, a revoked token keeps working until maxAge elapses. A reachable API that refuses the token deletes the entry immediately.

Security posture

caution

ESO writes decrypted values into Kubernetes Secret objects (base64 in etcd). That is inherent to how ESO works — seekrit's control plane stays zero-knowledge, but the cluster becomes a trusted decryption endpoint, the same as running seekrit-run on a CI box.

Harden accordingly:

  • Enable etcd encryption-at-rest so the synced Secrets aren't stored in plaintext.
  • Restrict who can reach the sidecar. It decrypts everything the token can read, gated by an API key. Turn on the chart's NetworkPolicy to allow ingress only from your ESO pods:
    --set networkPolicy.enabled=true \
    --set 'networkPolicy.from[0].namespaceSelector.matchLabels.kubernetes\.io/metadata\.name=external-secrets'
    
  • One token, one environment. The token's scope is the sidecar's blast radius. To sync another app or environment, install a second release with its own token and secretStore.name — don't broaden one token.

Reference