BlogSecrets

External Secrets Operator: sync Vault into Kubernetes

Keep the source of truth in Vault and let the operator materialize Kubernetes Secrets your pods can mount.

Jul 29, 2025·4 min readIntermediate·By the SecOpsLog team · command-tested

Run kubectl get secret db-credentials -o yaml and you see a base64 password any namespace admin can decode. That is where most applications actually consume secrets — mounted as files, injected as env vars, copied into crash dumps. External Secrets Operator (ESO) keeps the authoritative copy in Vault (or AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) and materializes a native Kubernetes Secret on a refresh interval. Your Deployment stays dumb: it mounts db-credentials like always. The operator is the glue.

This note installs ESO, wires a namespace-scoped SecretStore that authenticates to Vault with Kubernetes auth, declares an ExternalSecret that maps remote paths to keys, and verifies sync status. For the Vault side — policies, Kubernetes auth roles, and least-privilege paths — pair this with Vault from dev to production.

External store → Kubernetes Secret

ESO syncs copies into the cluster. Rotate at the source of truth.

Vault / AWS external store External Secrets Operator SecretStore + ExternalSecret K8s Secret → Pod mount 1 SecretStore how to reach the backend auth: IRSA / Vault SA / keys cluster- or namespace-scoped 2 ExternalSecret which keys + refreshInterval maps remote → Secret data controller reconciles loop 3 Synced Secret native K8s Secret object mount / env as usual rotate at the store, not YAML ESO copies values into the cluster. The source of truth stays outside Kubernetes. store → SecretStore → ExternalSecret → Secret → Pod
Store — sourceESO — syncSecret — consume
External Secrets sync loop

ESO reconciles on an interval. A rotated value in Vault becomes a new Kubernetes Secret version — pods still need a reload strategy to pick it up.

1Vault KVsource of truth2SecretStoreauth + server URL3ExternalSecret CRremoteRef mapping4ESO controllerfetch + compare5K8s Secretmaterialized in etcd6Pod volume/envapp consumes normally7refreshIntervalre-sync on timer

Install ESO and confirm the controller

Pin a release version from the official Helm chart or static manifests — floating latest on something that writes Secrets is a supply-chain gamble. After install, the controller pod in external-secrets namespace should be Ready before you apply stores.

bash — install and smoke testlive
helm repo add external-secrets https://charts.external-secrets.io
helm install eso external-secrets/external-secrets -n external-secrets --create-namespace
kubectl get pods -n external-secrets
external-secrets-xxxx 1/1 Running

Point a SecretStore at Vault with Kubernetes auth

A SecretStore is namespace-scoped — each team brings its own Vault role and mount. A ClusterSecretStore shares one backend cluster-wide; use it only when platform owns the trust boundary. The Kubernetes auth block tells ESO to exchange the pod's projected service account token for a Vault token limited by policy.

secretstore.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-prod
namespace: payments
spec:
provider:
vault:
server: "https://vault.acme.internal:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "payments-eso"
serviceAccountRef:
name: external-secrets-sa

Declare what to sync with an ExternalSecret

The data block maps Kubernetes Secret keys to Vault paths and properties. Set refreshInterval short enough that rotation matters — one hour is a common starting point — and wire Stakater Reloader or a similar tool if your app caches env vars at startup. For bulk imports, dataFrom with extract pulls every key under a Vault path into one Secret — handy for legacy apps that expect a dozen env vars from a single mount.

Grant the ESO service account only the Vault policy it needs: read on database/payments, not database/*. A compromised payments namespace should not become a cluster-wide secret reader because someone copy-pasted an admin policy during the pilot.

externalsecret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payments-db
namespace: payments
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-prod
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: database/payments
property: username
- secretKey: password
remoteRef:
key: database/payments
property: password
bash — confirm sync statuslive
kubectl apply -f secretstore.yaml -f externalsecret.yaml
kubectl get externalsecret -n payments payments-db
NAME STORE REFRESH STATUS READY
payments-db vault-prod 1h SecretSynced True
kubectl get secret -n payments db-credentials
materialized Secret exists — know your etcd exposure window
ESO does not encrypt etcd
Syncing from Vault into a Kubernetes Secret still leaves base64-encoded values in etcd unless you enable encryption at rest on the API server. ESO removes copy-paste sprawl; it is not a substitute for etcd encryption, RBAC, or short-lived dynamic credentials.
Three Kubernetes secret delivery patterns
Vault Agent injector
Sidecar fetches at runtime
No Secret object for static creds
More moving parts per pod
External Secrets (ESO)
Ordinary K8s Secret
Apps unchanged
Refresh interval + reloader
Pairs with GitOps manifests

Where this goes next

Static passwords in KV are a stepping stone. Next: dynamic database credentials from Vault's database secrets engine so each pod gets a unique lease, and Kubernetes auth policies tight enough that a compromised namespace cannot read another team's paths. The Advanced secrets track covers injector vs CSI vs ESO trade-offs and rotation without midnight restarts.

Go deeper in a courseVault from dev to productionAuth methods, dynamic secrets, Kubernetes integration, and HA.View course

Related posts