External Secrets Operator + HashiCorp Vault: Kubernetes auth flow
Published: 2026-03-19
SealedSecrets work well for most cluster credentials. But some secrets — like shared service credentials managed by a security team — need to live in Vault and be automatically synchronized into Kubernetes. External Secrets Operator (ESO) bridges the two.
The components
- HashiCorp Vault — runs on
infracluster, deployed via a customcharts/vault/Helm chart. - External Secrets Operator — deployed via
base/external-secrets/helmrelease.yamlon infra. - ClusterSecretStore — cluster-scoped ESO resource that defines how to connect to Vault.
- ExternalSecret — defines which Vault path to sync into which Kubernetes Secret.
ESO HelmRelease
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-secrets
spec:
chart:
spec:
chart: external-secrets
version: "0.14.4"
sourceRef:
kind: HelmRepository
name: external-secrets
namespace: flux-system
values:
installCRDs: true
replicaCount: 1
serviceMonitor:
enabled: true
interval: 60s
labels:
release: kube-prom-stack
Vault Kubernetes auth setup
Before ESO can authenticate to Vault, the Kubernetes auth method must be configured once per cluster:
bashvault auth enable -path=kubernetes-infra kubernetes
vault write auth/kubernetes-infra/config \
kubernetes_host="https://172.16.57.10:6443" \
kubernetes_ca_cert=@/tmp/infra-ca.crt \
token_reviewer_jwt="$(kubectl create token -n external-secrets external-secrets)"
vault policy write infra-default - <<EOF
path "secret/data/infra/*" { capabilities = ["read"] }
EOF
vault write auth/kubernetes-infra/role/external-secrets \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
policies=infra-default \
ttl=1h
The token_reviewer_jwt is used by Vault to call the Kubernetes TokenReview API and validate incoming tokens.
ClusterSecretStore
yamlapiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-infra
spec:
provider:
vault:
server: "http://vault.vault.svc.cluster.local:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes-infra"
role: "external-secrets"
serviceAccountRef:
name: "external-secrets"
namespace: "external-secrets"
Vault is accessible inside the infra cluster by its service ClusterIP. No external endpoint needed.
ExternalSecret
yamlapiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: reportportal-creds
namespace: infra
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-infra
kind: ClusterSecretStore
target:
name: reportportal-creds
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: secret/infra/reportportal
property: password
ESO pulls secret/infra/reportportal from Vault's KV v2 engine every hour and creates/updates the reportportal-creds Secret. creationPolicy: Owner means if the ExternalSecret is deleted, the Secret is deleted too.
Syncing multiple fields from one Vault path
yamlspec:
dataFrom:
- extract:
key: secret/infra/postgres
dataFrom.extract pulls all fields from the Vault path as separate keys in the Secret. Useful when a Vault path contains username, password, host, port — all become separate Secret keys.
Sealed Secrets vs External Secrets
| Use case | Sealed Secrets | External Secrets |
|---|---|---|
| kubeconfig files | ✓ | — |
| Docker registry creds | ✓ | — |
| TLS certificates | ✓ | — |
| Shared service creds (DB passwords) | — | ✓ |
| Secrets rotated by security team | — | ✓ |
| Secrets that need audit trail | — | ✓ (via Vault audit) |
Both tools coexist in the same cluster.
Checking sync status
bashkubectl get externalsecret -A
kubectl describe externalsecret reportportal-creds -n infra
# Look for: Status.Conditions - Ready=True
SecretSyncedError fires if Vault is sealed or the path doesn't exist.
Troubleshooting
ESO cannot authenticate to Vault:
bashkubectl logs -n external-secrets deploy/external-secrets
# Look for: "authentication error"
Common cause: token_reviewer_jwt expired or the SA was recreated after auth setup.
Vault path not found:
bashvault kv get secret/infra/reportportal
The path in remoteRef.key must match exactly (without the data/ prefix for KV v2 — ESO handles this automatically).