HashiCorp Vault — External Secrets
The External Secrets Operator (ESO) syncs secrets from Vault into native Kubernetes Secret objects — ideal for applications that expect to read from K8s Secrets.
How It Works
1. ExternalSecret CR defines what to sync from Vault
2. ESO controller reads secrets from Vault
3. ESO creates/updates Kubernetes Secret objects
4. Pods use K8s Secrets normally (env vars, volumes)
5. ESO periodically refreshes (configurable interval)
Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace
Configure Vault Connection
SecretStore (namespace-scoped)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: default
spec:
provider:
vault:
server: "http://vault.vault.svc.cluster.local:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: "external-secrets-sa"
ClusterSecretStore (cluster-wide)
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-cluster
spec:
provider:
vault:
server: "http://vault.vault.svc.cluster.local:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: "external-secrets-sa"
namespace: "external-secrets"
Sync Secrets
Single Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-db-secret
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-db-secret # K8s Secret name
creationPolicy: Owner # ESO owns the Secret
data:
- secretKey: username # Key in K8s Secret
remoteRef:
key: myapp/db # Vault path (under secret/)
property: username # Key in Vault secret
- secretKey: password
remoteRef:
key: myapp/db
property: password
All Keys from a Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-config
spec:
refreshInterval: 30m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-config
dataFrom:
- extract:
key: myapp/config # Sync ALL keys from this Vault path
Use the Synced Secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
envFrom:
- secretRef:
name: myapp-config # All keys as env vars
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-db-secret
key: password
Vault Setup for ESO
# Create policy for ESO
vault policy write external-secrets - <<EOF
path "secret/data/*" {
capabilities = ["read"]
}
path "secret/metadata/*" {
capabilities = ["read", "list"]
}
EOF
# Create Kubernetes auth role
vault write auth/kubernetes/role/external-secrets \
bound_service_account_names=external-secrets-sa \
bound_service_account_namespaces=external-secrets \
policies=external-secrets \
ttl=1h
# Service account for ESO
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-secrets-sa
namespace: external-secrets
ESO vs Agent Injector vs CSI
| Feature | ESO | Agent Injector | CSI Provider |
|---|---|---|---|
| Creates K8s Secrets | ✅ Yes | ❌ No | ✅ Optional |
| Sidecar needed | ❌ No | ✅ Yes | ❌ No |
| Auto-refresh | ✅ Configurable interval | ✅ Continuous | ❌ On pod restart |
| Templates | ❌ Limited | ✅ Full Go template | ❌ No |
| Dynamic secrets | ❌ Not ideal | ✅ Yes (with renewal) | ❌ Limited |
| Best for | Static secrets, env vars | Dynamic secrets, files | Volume-mounted secrets |
Quick Reference
| Resource | Purpose |
|---|---|
SecretStore |
Namespace-scoped Vault connection |
ClusterSecretStore |
Cluster-wide Vault connection |
ExternalSecret |
Defines what to sync |
refreshInterval |
How often to re-sync |
Next: Practice Exercises