Secrets Management - Street-Level Ops¶
Real-world patterns and gotchas from production secrets management.
Quick Diagnosis Commands¶
# Check if a secret exists and its age
kubectl get secret <name> -n <ns> -o jsonpath='{.metadata.creationTimestamp}'
# Decode a specific key
kubectl get secret <name> -n <ns> -o jsonpath='{.data.<key>}' | base64 -d
# List all secrets in a namespace (names only, no values)
kubectl get secrets -n <ns>
# Check Sealed Secrets controller status
kubectl logs -n kube-system deploy/sealed-secrets-controller --tail=50
# Check ESO sync status
kubectl get externalsecret -n <ns>
# Vault: check auth health
kubectl exec -n vault vault-0 -- vault status
Gotcha: Pods Don't Pick Up Secret Changes¶
Kubernetes does NOT restart pods when a Secret changes. The pod still has the old value.
Workarounds:
# Option 1: Rolling restart
kubectl rollout restart deployment/<name> -n <ns>
# Option 2: Use Reloader (auto-restarts on secret change)
# https://github.com/stakater/Reloader
helm install reloader stakater/reloader -n kube-system
# Annotate your deployment
metadata:
annotations:
reloader.stakater.com/auto: "true"
# Option 3: Volume-mounted secrets auto-update (but app must re-read)
# Kubelet refreshes mounted secrets every ~60s
Gotcha: Sealed Secrets Key Backup¶
If you lose the Sealed Secrets controller key, all your SealedSecrets are permanently unrecoverable.
# BACK UP THE KEY
kubectl get secret -n kube-system sealed-secrets-key -o yaml > sealed-secrets-key-backup.yaml
# Store this backup in a secure location (NOT in the same Git repo)
# Vault, AWS Secrets Manager, or a physical safe
Under the hood: Kubernetes Secrets are stored in etcd as base64-encoded plaintext by default. Anyone with
kubectl get secretRBAC permission can read them. Enable etcd encryption at rest (--encryption-provider-config) to encrypt Secrets in etcd with AES-CBC or AES-GCM. Without this, a compromised etcd backup exposes every Secret in the cluster.
Gotcha: ESO RefreshInterval vs Pod Restart¶
ESO updates the K8s Secret on refreshInterval, but pods don't see it unless:
1. The secret is mounted as a volume (kubelet refreshes ~60s)
2. You use Reloader to trigger a rollout
3. Your app watches the file for changes
Best practice: Set refreshInterval: 1h and use Reloader for automatic pod restarts.
Pattern: Secret Rotation Checklist¶
- Generate new credential in external store (Vault/AWS SM)
- Verify ESO syncs the new value:
kubectl get externalsecret -n <ns> - Rolling restart the deployment
- Verify app works with new credential
- Revoke old credential in external store
- Check no other services used the old credential
Pattern: Namespace Isolation for Secrets¶
# SecretStore per namespace (recommended over ClusterSecretStore)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: team-secrets
namespace: team-a
spec:
provider:
vault:
server: https://vault.example.com
path: secret/data/team-a # Scoped path
This ensures Team A can only access their own secrets in Vault.
Pattern: Multi-Tenant Secret Access¶
# Vault policy per team
vault policy write team-a - <<EOF
path "secret/data/team-a/*" {
capabilities = ["read", "list"]
}
path "secret/data/shared/*" {
capabilities = ["read"]
}
EOF
Gotcha: Vault auto-unseal (via AWS KMS, Azure Key Vault, or Transit) generates recovery keys instead of unseal keys. Recovery keys CANNOT unseal Vault if the auto-unseal provider is unavailable — they are authorization-only. If your KMS key is deleted, Vault is permanently sealed. Always back up both the recovery keys AND ensure KMS key deletion protection is enabled.
Emergency: Secret Leaked¶
# 1. ROTATE IMMEDIATELY (don't investigate first)
# Rotate the credential in the external store
# 2. Check who accessed it
kubectl get events -n <ns> --field-selector reason=Get,involvedObject.name=<secret-name>
# 3. Audit Git history
git log --all -p -- '*.yaml' | grep -i password # Check for committed secrets
# 4. If in Git, clean history (git-filter-repo is the recommended tool, faster than BFG)
pip install git-filter-repo
git filter-repo --path-glob '*.secret.yaml' --invert-paths
# 5. Add prevention
git secrets --install
git secrets --register-aws
Anti-Pattern: Environment Variables for Secrets¶
# AVOID: secrets visible in kubectl describe, process listing, crash dumps
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
# PREFER: volume-mounted secrets
volumes:
- name: secrets
secret:
secretName: db-creds
containers:
- name: app
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
# App reads from /etc/secrets/password
Volume-mounted secrets: auto-update, not in kubectl describe, not in /proc/*/environ.
War story: A team stored database passwords as environment variables. During an incident, an engineer ran
kubectl describe podand pasted the output into a public Slack channel. The password was visible in theEnvironmentsection. After rotating the credential, they switched to volume-mounted secrets and addedkubectl describeoutput to their secret-scanning tool's detection patterns.
Quick Reference¶
- Cheatsheet: Secrets-Management
- Runbook: Secret Rotation