Skip to content

External Secrets Operator (ESO)

Automatically sync secrets from HashiCorp Vault into native Kubernetes Secrets.

Time: ~15 minutes Difficulty: Intermediate

  • External Secrets Operator: the bridge between external secret stores and Kubernetes
  • SecretStore: connecting ESO to Vault
  • ExternalSecret: declaring which Vault secrets become K8s Secrets
  • Automatic sync and refresh intervals
  • data vs dataFrom for selective vs full secret extraction
  • Apps consume normal K8s Secrets with no Vault SDK needed

the Vault demo must be running with secrets already stored. If you haven’t done that:

Terminal window
task demo -- vault
# Then follow the "Store Secrets" section in demos/vault/README.md

The previous demo showed how pods can authenticate to Vault and read secrets via the API. That works, but it means every app needs Vault-aware code. ESO removes that coupling:

  • Apps use standard secretKeyRef, no Vault SDK or API calls
  • ESO handles authentication, syncing, and refresh in the background
  • Changing the secret store (Vault to AWS Secrets Manager, for example) requires updating the SecretStore, not the application

Navigate to the demo directory:

Terminal window
cd demos/external-secrets
Terminal window
kubectl apply -f manifests/namespace.yaml
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
--namespace eso-demo \
--set installCRDs=true

Wait for the operator to be ready:

Terminal window
kubectl get pods -n eso-demo -w

Create a SecretStore that points to the Vault instance from demo 28:

Terminal window
kubectl apply -f manifests/secret-store.yaml

Verify the connection:

Terminal window
kubectl get secretstore -n eso-demo

The STATUS column should show Valid.

Terminal window
kubectl apply -f manifests/external-secret-db.yaml

This pulls individual fields from secret/demo/database in Vault and maps them to specific keys in a K8s Secret.

Terminal window
kubectl apply -f manifests/external-secret-api.yaml

This pulls all keys from secret/demo/api in Vault using dataFrom.extract.

Terminal window
# Check ExternalSecret status
kubectl get externalsecrets -n eso-demo
# See the K8s Secrets that ESO created
kubectl get secrets -n eso-demo
# Read the synced values
kubectl get secret db-credentials -n eso-demo -o jsonpath='{.data.DB_PASSWORD}' | base64 -d && echo
kubectl get secret api-credentials -n eso-demo -o jsonpath='{.data.key}' | base64 -d && echo

The values match what you stored in Vault in demo 28.

Terminal window
kubectl apply -f manifests/app.yaml

Check the app logs:

Terminal window
kubectl logs deploy/demo-app -n eso-demo

The app reads database and API credentials from normal K8s Secrets. It has no idea Vault exists. If you swap Vault for AWS Secrets Manager tomorrow, you update the SecretStore and the app never changes.

Update a secret in Vault and watch ESO sync the change:

Terminal window
# Update the password in Vault
kubectl exec -it vault-0 -n vault-demo -- \
vault kv put secret/demo/database \
username=appuser \
password=new-password-12345 \
host=postgres.example.com \
port=5432
# Wait for the refresh interval (30 seconds for db-credentials)
sleep 35
# Check the K8s Secret was updated
kubectl get secret db-credentials -n eso-demo \
-o jsonpath='{.data.DB_PASSWORD}' | base64 -d && echo
# Output: new-password-12345

The K8s Secret is updated automatically. The app pod needs a restart to pick up the new env var value (env vars don’t hot-reload, but volume-mounted Secrets do).

manifests/
namespace.yaml # eso-demo namespace
secret-store.yaml # Vault token Secret + SecretStore CR
external-secret-db.yaml # ExternalSecret: selective field mapping
external-secret-api.yaml # ExternalSecret: extract all fields
app.yaml # App consuming the synced K8s Secrets

The full flow:

Vault (source of truth)
|
v ESO polls every refreshInterval
SecretStore (connection config)
|
v
ExternalSecret (declares what to sync)
|
v creates/updates
Kubernetes Secret (native K8s object)
|
v consumed by
Pod (via secretKeyRef or volume mount)

data vs dataFrom:

ModeUse CaseExternalSecret Field
dataPick specific keys, rename themdata[].secretKey + remoteRef.property
dataFrom.extractPull all keys from a pathdataFrom[].extract.key
  1. Delete the K8s Secret and watch ESO recreate it:

    Terminal window
    kubectl delete secret db-credentials -n eso-demo
    kubectl get externalsecrets -n eso-demo -w
    # ESO recreates it within the refresh interval
  2. Create an ExternalSecret with a template to format the output:

    Terminal window
    kubectl apply -f - <<'EOF'
    apiVersion: external-secrets.io/v1beta1
    kind: ExternalSecret
    metadata:
    name: db-connection-string
    namespace: eso-demo
    spec:
    refreshInterval: 30s
    secretStoreRef:
    name: vault-backend
    kind: SecretStore
    target:
    name: db-connection-string
    data:
    - secretKey: DSN
    remoteRef:
    key: demo/database
    property: username
    - secretKey: PASSWORD
    remoteRef:
    key: demo/database
    property: password
    - secretKey: HOST
    remoteRef:
    key: demo/database
    property: host
    EOF
  3. Check the ESO controller logs for sync activity:

    Terminal window
    kubectl logs -l app.kubernetes.io/name=external-secrets -n eso-demo --tail=20
Terminal window
helm uninstall external-secrets -n eso-demo
kubectl delete namespace eso-demo
# Also clean up Vault from demo 28 if done
helm uninstall vault -n vault-demo
kubectl delete namespace vault-demo

See docs/deep-dive.md for a detailed explanation of ClusterSecretStore for multi-namespace access, PushSecret for writing back to Vault, secret templates with Go templating, multiple providers (AWS, GCP, Azure), generator CRDs, and migration from sealed-secrets.

Move on to Tekton Basics to build cloud-native CI/CD pipelines.