External Secrets Operator
Sync secrets from external providers (AWS, Vault) into Kubernetes Secrets using CRDs.
Docs: https://external-secrets.io/latest/
Installation
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace \
--set installCRDs=true
CRDs require server-side apply (they exceed 256KB):
kubectl apply --server-side -f \
"https://raw.githubusercontent.com/external-secrets/external-secrets/v0.14.3/deploy/crds/bundle.yaml"
Verify:
kubectl -n external-secrets get pods
kubectl get crd externalsecrets.external-secrets.io
Core Concepts
SecretStore vs ClusterSecretStore
| SecretStore | ClusterSecretStore | |
|---|---|---|
| Scope | Single namespace | Cluster-wide |
| Auth secrets | Same namespace only | Any namespace via namespace field |
| Use when | Team-owned, namespace isolation | Shared platform credentials |
Provider Configurations
AWS Secrets Manager
SecretStore (namespace-scoped)
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secrets
namespace: my-app
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-creds
key: access-key
secretAccessKeySecretRef:
name: aws-creds
key: secret-key
IRSA (preferred on EKS)
Omit the auth block entirely and set role:
spec:
provider:
aws:
service: SecretsManager # or ParameterStore
region: us-east-1
role: arn:aws:iam::123456789:role/eso-role
AWS Parameter Store
Same provider block, just change the service:
spec:
provider:
aws:
service: ParameterStore
region: us-east-1
HashiCorp Vault (On-Prem)
ClusterSecretStore with Kubernetes Auth
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: my-sa
AppRole Auth
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
appRole:
path: "approle"
roleRef:
name: vault-approle
key: role-id
secretRef:
name: vault-approle
key: secret-id
Token Auth (dev/simple setups)
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
tokenSecretRef:
name: vault-token
key: token
Vault gotchas:
- KV v2 path: the actual API path is
<mount>/data/<key>— ESO handles this whenversion: "v2"is set. Don't add/data/yourself - Vault namespaces (Enterprise): set
spec.provider.vault.namespace - TLS: use
spec.provider.vault.caBundleorcaProviderfor self-signed certs
ExternalSecret
Individual Keys (data)
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
data:
- secretKey: db-password # Key in K8s Secret
remoteRef:
key: prod/database # Key in provider
property: password # JSON property (if structured)
- secretKey: api-key
remoteRef:
key: prod/api-keys
property: stripe
Full Secret Sync (dataFrom)
spec:
dataFrom:
# Extract all keys from a single secret
- extract:
key: prod/database
# Find multiple secrets by name/tags
- find:
name:
regexp: "prod/.*"
# AWS tags filter:
# tags:
# environment: production
Both Together
spec:
data:
- secretKey: extra-key
remoteRef:
key: prod/extra
dataFrom:
- extract:
key: prod/database
Templating
Transform secret data before storing in Kubernetes:
spec:
target:
template:
engineVersion: v2
type: Opaque # or kubernetes.io/tls, kubernetes.io/dockerconfigjson
data:
DATABASE_URL: |
postgresql://{{ .username }}:{{ .password }}@{{ .host }}:5432/{{ .dbname }}
decoded: "{{ .encoded_value | b64dec }}"
.dockerconfigjson: |
{"auths":{"registry.example.com":{"username":"{{ .user }}","password":"{{ .pass }}"}}}
data:
- secretKey: username
remoteRef:
key: db-creds
property: username
- secretKey: password
remoteRef:
key: db-creds
property: password
- secretKey: host
remoteRef:
key: db-creds
property: host
- secretKey: dbname
remoteRef:
key: db-creds
property: dbname
Template functions: standard Go template functions plus b64enc, b64dec, upper, lower, replace, trim, now, hasPrefix, hasSuffix.
Helm escaping: wrap ESO templates in backticks when deploying via Helm:
password: "{{ `{{ .mysecret }}` }}"
Troubleshooting
Check Status
kubectl get secretstores -A
kubectl get clustersecretstores
kubectl get externalsecrets -A
kubectl describe externalsecret <name> -n <ns>
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
SecretSyncedError | Provider auth failed | Check credentials in referenced Secret |
SecretStore NotReady | Connection/auth issue | kubectl describe secretstore <name> |
| Secret not updating | refreshInterval too long or CreatedOnce policy | Check refreshInterval and refreshPolicy |
InvalidStoreRef | Wrong store name/kind | Verify secretStoreRef matches existing store |
| CRD install fails | Over 256KB limit | Use kubectl apply --server-side |
| RBAC errors | Missing permissions | Ensure ESO SA has access to auth secrets |
Diagnostic Script
Run scripts/eso-diagnostics.sh for comprehensive health checks.
Reference Docs
- Vault Integration Deep-Dive: AppRole auth, Kubernetes auth, KV v2 paths, namespaces
- PushSecret & Multi-Tenant Patterns: Reverse sync with PushSecret, namespace isolation, RBAC patterns
- Generators: Dynamic secret generation — ECR pull secret auto-rotation, STS session tokens, Vault dynamic secrets, password generation, ClusterGenerator patterns
References
generators.md— Dynamic secret generation and token rotation with ESO generatorspushsecret-multitenant.md— PushSecret patterns and multi-tenant secret managementvault-integration.md— Vault authentication methods, token rotation, and PKI integration
Related Skills
- helm — Helm secrets handling patterns and ESO integration in charts
- argocd — GitOps deployment of ExternalSecret/SecretStore resources
- crossplane — Crossplane provider credentials via ESO
- cert-manager — Store CA private keys in external secret managers
