Kubernetes Secrets solve the "how do I inject config into Pods" problem, but they don't solve the "where do secrets live and who manages their lifecycle" problem. In production, secrets live in dedicated secret management systems — Vault, AWS Secrets Manager, GCP Secret Manager — and are synced into Kubernetes. This lesson covers the ecosystem and patterns.
1. Why External Secrets Management?
K8s native Secrets have fundamental limitations for production:
| Problem | Why K8s Secrets Alone Fail |
|---|---|
| Storage in Git | You can't commit Secret YAML to Git (even base64 is readable) |
| Rotation | No built-in automatic rotation — manual process |
| Audit trail | Who changed a Secret? When? K8s audit logs are limited |
| Cross-environment | Same secret source for dev/staging/prod — K8s Secrets are per-cluster |
| Access control | RBAC is namespace-level — no per-secret access policies |
| Dynamic secrets | No concept of short-lived credentials (e.g., DB passwords that expire) |
The Solution Landscape
2. External Secrets Operator (ESO)
The most popular approach. ESO is a K8s operator that syncs secrets from external stores into native K8s Secrets. It uses Custom Resources to declare what to sync.
Architecture
# 1. SecretStore — HOW to connect to the external provider # 2. ExternalSecret — WHAT to fetch and WHERE to put it # 3. Result: a native K8s Secret (created/updated by ESO controller)
SecretStore (or ClusterSecretStore)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-service-account # Uses IRSA (IAM Roles for SAs)
ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # ← How often to sync from external store
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-creds # ← K8s Secret to create
creationPolicy: Owner # ESO owns (deletes when ExternalSecret deleted)
data:
- secretKey: username # ← key in the K8s Secret
remoteRef:
key: prod/database # ← path in AWS Secrets Manager
property: username # ← JSON field within the secret
- secretKey: password
remoteRef:
key: prod/database
property: password
What Happens
- ESO controller reads the
ExternalSecretCR - Connects to AWS Secrets Manager using the
SecretStorecredentials - Fetches
prod/databaseand extractsusernameandpassword - Creates (or updates) K8s Secret
db-credsin namespaceproduction - Every
refreshInterval, re-syncs (catches rotations in the external store)
Supported Providers
| Provider | Service |
|---|---|
| AWS | Secrets Manager, Parameter Store (SSM) |
| GCP | Secret Manager |
| Azure | Key Vault |
| HashiCorp | Vault (KV v1/v2, PKI, dynamic secrets) |
| Other | IBM Cloud, Oracle, Doppler, 1Password, CyberArk, Delinea |
ClusterSecretStore (cluster-scoped) when the same external provider serves multiple namespaces. Use namespace-scoped SecretStore for multi-tenant clusters where teams bring their own credentials to different secret stores.
3. Alternative Approaches
Sealed Secrets (Bitnami)
Encrypt Secrets client-side so they can be stored in Git safely.
# Install kubeseal CLI + controller in cluster # Encrypt a Secret: kubectl create secret generic db-creds \ --from-literal=password=s3cr3t \ --dry-run=client -o yaml | kubeseal \ --controller-name=sealed-secrets \ --controller-namespace=kube-system \ -o yaml > sealed-secret.yaml # sealed-secret.yaml can be committed to Git safely # The SealedSecret controller decrypts it inside the cluster # and creates a regular K8s Secret
| Pros | Cons |
|---|---|
| Secrets in Git (GitOps-native) | No rotation support |
| Simple — no external system needed | Sealed to one cluster (can't reuse across clusters) |
| Encryption with cluster's public key | If cluster key is lost, all secrets unrecoverable |
Secrets Store CSI Driver
Mounts secrets from external stores directly into Pods as volumes — bypasses K8s Secrets entirely.
# SecretProviderClass defines what to fetch:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-db-creds
spec:
provider: vault
parameters:
vaultAddress: "https://vault.company.com"
roleName: "web-app"
objects: |
- objectName: "db-password"
secretPath: "secret/data/db"
secretKey: "password"
# Pod mounts it:
spec:
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-db-creds
containers:
- volumeMounts:
- name: secrets
mountPath: /mnt/secrets
readOnly: true
| Pros | Cons |
|---|---|
| Secrets never stored in etcd at all | More complex setup (CSI driver + provider) |
| Direct integration with Vault, AWS, GCP, Azure | Secrets only available as files (or synced to K8s Secret optionally) |
| Automatic rotation via provider | Pod must tolerate file changes during runtime |
SOPS + GitOps (Flux/ArgoCD)
Encrypt secret values in YAML files using SOPS (Mozilla). GitOps controller decrypts on apply.
# Encrypt specific fields in a Secret YAML: sops --encrypt --in-place --encrypted-regex '^(data|stringData)$' secret.yaml # Flux or ArgoCD configured with decryption provider # On apply: decrypts in memory → creates K8s Secret # Secret YAML committed to Git (encrypted values only)
HashiCorp Vault — Direct Integration
Vault offers the Vault Agent Injector: a mutating webhook that injects a Vault sidecar into Pods. The sidecar fetches secrets from Vault and writes them to a shared volume.
# Annotations trigger the injector:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "web-app"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/db"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "secret/data/db" -}}
export DB_PASS={{ .Data.data.password }}
{{- end -}}
4. Choosing the Right Approach
| Approach | Source of Truth | Rotation | GitOps | Complexity | Best For |
|---|---|---|---|---|---|
| ESO | External store | ✅ Auto-sync | ✅ (CRs in Git) | Medium | Most production setups |
| Sealed Secrets | Git (encrypted) | ❌ Manual | ✅ Native | Low | Small teams, no external store |
| CSI Driver | External store | ✅ Provider-driven | ✅ (CRs in Git) | High | Zero-etcd-storage requirement |
| SOPS + GitOps | Git (encrypted) | ❌ Manual | ✅ Native | Low-Med | GitOps-first teams |
| Vault Agent | Vault | ✅ Dynamic | ⚠️ Annotations | High | Dynamic secrets, PKI, advanced Vault features |
Decision Flowchart
Do you have an external secret store (Vault/AWS/GCP/Azure)?
├─ YES → Do you need secrets that never touch etcd?
│ ├─ YES → Secrets Store CSI Driver
│ └─ NO → External Secrets Operator (simplest, most flexible)
└─ NO → Do you use GitOps?
├─ YES → Sealed Secrets or SOPS
└─ NO → Native K8s Secrets + encryption at rest
5. Production Patterns
Secret Rotation Flow (ESO)
- Secret rotated in AWS Secrets Manager (manual or Lambda-based auto-rotation)
- ESO syncs new value on next
refreshInterval - K8s Secret updated
- If volume-mounted: kubelet syncs file in ~60s → app reads new value
- If env var:
kubectl rollout restartneeded (or config hash annotation pattern)
Multi-Cluster Secret Sync
# Same ExternalSecret in all clusters pointing to same AWS path: # dev cluster: remoteRef.key = dev/database # prod cluster: remoteRef.key = prod/database # Single source of truth per environment # Rotation in AWS propagates to all clusters automatically
Summary
| Concept | Key Point |
|---|---|
| External store | Source of truth for secrets — handles rotation, audit, access control |
| ESO | Operator that syncs external secrets → K8s Secrets via CRDs |
| SecretStore | Connection config to the external provider |
| ExternalSecret | Declares what to fetch, where to put it, how often to sync |
| refreshInterval | How often ESO re-syncs (catches rotations) |
| Sealed Secrets | Encrypt secrets for Git — no external store needed |
| CSI Driver | Mount secrets directly from external store — bypasses etcd |
| Vault Agent | Sidecar injects dynamic secrets — short-lived credentials |
📝 Quiz: External Secrets Management
Q1: A database password is rotated in AWS Secrets Manager. ESO has refreshInterval: 1h. When does the K8s Secret update?
refreshInterval (e.g., 5m), but this increases API calls to the external store.Q2: What's the main advantage of Secrets Store CSI Driver over ESO?
Q3: You're using Sealed Secrets. You need to rotate a database password. What's the process?
kubeseal. (3) Commit to Git. (4) GitOps controller applies it, creating an updated K8s Secret. (5) Restart Pods if using env vars. Sealed Secrets have no automatic rotation — this is their main limitation.Q4: What's the difference between SecretStore and ClusterSecretStore in ESO?
SecretStore is namespaced — only ExternalSecrets in the same namespace can reference it. ClusterSecretStore is cluster-scoped — any ExternalSecret in any namespace can reference it. Use ClusterSecretStore for a shared provider; use SecretStore for multi-tenant isolation (each team has their own store credentials).Q5: When would you choose Vault Agent Injector over ESO with a Vault provider?
Q6: Your team uses GitOps (ArgoCD). What can you safely commit to Git?