Kubernetes Secrets provide the foundation for managing sensitive data in containerized environments, yet many teams struggle to move beyond basic key-value storage into production-grade patterns. This guide covers advanced Secret management concepts including encryption at rest, external secret operators, rotation strategies, and audit-ready configurations. Each section includes concrete commands, expected outputs, and verification steps so you can apply these patterns safely in your clusters.
Encryption at Rest and Provider Configuration
Kubernetes stores Secrets unencrypted in etcd by default. Enabling encryption at rest requires configuring the kube-apiserver with an EncryptionConfiguration resource that defines which resources to encrypt and which provider to use. The supported providers include aescbc, aesgcm, secretbox, and kms — with kms being the only option that keeps encryption keys out of the control plane entirely.
Create an encryption configuration file at /etc/kubernetes/encryption-config.yaml:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
- kms:
name: myKMSProvider
endpoint: unix:///var/run/kms-plugin/socket.sock
cachesize: 1000
timeout: 3s
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGtleSBtYXR0ZXJzIGhlcmU=
Update the kube-apiserver manifest (/etc/kubernetes/manifests/kube-apiserver.yaml) to reference this file:
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
After the apiserver restarts, verify encryption is active by creating a test Secret and examining the raw etcd data:
kubectl create secret generic test-encryption --from-literal=token=my-secret-value
ETCDCTL_API=3 etcdctl get /registry/secrets/default/test-encryption --print-value-only | xxd | head -20
The output should show binary data without the plaintext my-secret-value visible. If you see the plaintext, encryption is not working — check kube-apiserver logs for encryption related errors.
Key rotation: To rotate the encryption key, add a new key to the keys list with name: key2 and place it first. Restart the apiserver. Then re-encrypt existing Secrets:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Monitor the re-encryption progress with kubectl get secrets --all-namespaces -o custom-columns=NAME:.metadata.name,ENCRYPTED:.metadata.annotations.kubernetes\.io/encrypted.
External Secrets Operator and Secret Synchronization
Storing secrets in Kubernetes etcd creates a single point of failure and complicates multi-cluster synchronization. The External Secrets Operator (ESO) fetches secrets from external providers — AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager — and materializes them as Kubernetes Secrets.
Install ESO via Helm:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
Configure a ClusterSecretStore for AWS Secrets Manager with IRSA (IAM Roles for Service Accounts):
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets-system
Create the service account and annotate it with the IAM role ARN:
kubectl annotate sa external-secrets-sa -n external-secrets-system \
eks.amazonaws.com/role-arn=arn:aws:iam::123456789012:role/ExternalSecretsRole
Now define an ExternalSecret that syncs a specific secret from AWS:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-creds
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: prod/database
property: username
- secretKey: password
remoteRef:
key: prod/database
property: password
Verify synchronization:
kubectl get externalsecret database-credentials -n production -o yaml
kubectl get secret db-creds -n production -o jsonpath='{.data.username}' | base64 -d
The refreshInterval controls how often ESO polls the provider. For immediate propagation after a secret change in AWS, trigger a manual refresh:
kubectl annotate externalsecret database-credentials -n production force-refresh=$(date +%s) --overwrite
Failure mode: If the external provider is unreachable, ESO retains the last successfully synced Secret. Check kubectl describe externalsecret for Condition: Ready status and LastRefreshTime. Set up Prometheus alerts on external_secrets_last_refresh_timestamp_seconds staleness.
Automated Secret Rotation with Reloader and CSI Drivers
Rotating secrets without pod restarts requires either a sidecar that watches for changes or the Secrets Store CSI Driver with rotation enabled. The CSI driver mounts secrets as files and can refresh them on a configurable interval.
Install the Secrets Store CSI Driver and the provider for your backend (example: AWS):
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi secrets-store-csi-driver/secrets-store-csi-driver -n kube-system --set syncSecret.enabled=true --set enableSecretRotation=true
Create a SecretProviderClass referencing the AWS provider:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-secrets
namespace: production
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/api-key"
objectType: "secretsmanager"
jmesPath:
- path: api_key
objectAlias: api-key
secretObjects:
- secretName: api-credentials
type: Opaque
data:
- objectName: api-key
key: api-key
Mount the CSI volume in your Deployment:
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: aws-secrets
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets"
readOnly: true
With enableSecretRotation=true, the driver polls the provider every 2 minutes (configurable via rotationPollInterval) and updates the mounted files and the synced Kubernetes Secret. Applications reading from /mnt/secrets/api-key see the new value without restart.
Verification: Update the secret in AWS Secrets Manager, then watch the pod:
kubectl exec -it <pod-name> -n production -- cat /mnt/secrets/api-key
kubectl get secret api-credentials -n production -o jsonpath='{.data.api-key}' | base64 -d
Both should reflect the new value within the polling interval. If not, check the CSI driver pods: kubectl logs -n kube-system -l app=secrets-store-csi-driver.
Alternative for non-CSI environments: Use the stakater/reloader deployment annotation to trigger rolling restarts when a Secret changes:
annotations:
reloader.stakater.com/auto: "true"
This restarts pods when any referenced Secret or ConfigMap changes — simpler but causes brief downtime during rollout.
RBAC, Audit Logging, and Namespace Scoping
Secrets are high-value targets. Restrict access with least-privilege RBAC and enable audit logging to detect anomalous access patterns.
Create a Role that allows reading only specific Secrets in a namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader-limited
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-creds", "api-credentials", "tls-cert"]
verbs: ["get", "list", "watch"]
Bind it to a ServiceAccount used by your application:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: secret-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app-sa
namespace: production
roleRef:
kind: Role
name: secret-reader-limited
apiGroup: rbac.authorization.k8s.io
Audit policy: Configure the kube-apiserver audit policy to log Secret access at the Metadata or Request level. Create /etc/kubernetes/audit-policy.yaml:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Request
resources:
- group: ""
resources: ["secrets"]
namespaces: ["production", "staging"]
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
Mount and enable in kube-apiserver:
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
Query audit logs for suspicious activity:
grep '"resource":"secrets"' /var/log/kubernetes/audit.log | jq -r '.user.username + " " + .verb + " " + .objectRef.name' | sort | uniq -c | sort -rn
This surfaces users or service accounts accessing an unusual number of Secrets. Alert on verb: "get" OR "list" from service accounts that should only watch.
Namespace isolation: Use ResourceQuota to limit Secret count per namespace and prevent Secret sprawl:
apiVersion: v1
kind: ResourceQuota
metadata:
name: secret-quota
namespace: production
spec:
hard:
secrets: "50"
Immutable Secrets and Versioned Rollout Patterns
Kubernetes 1.19+ supports immutable Secrets. Once marked immutable, a Secret cannot be modified or deleted — only replaced by creating a new Secret with a different name. This prevents accidental overwrites and enables safe rollback patterns.
Create an immutable Secret:
kubectl create secret generic app-config-v1 --from-literal=api-endpoint=https://api.v1.example.com --immutable
Update the Deployment to reference the versioned Secret name:
envFrom:
- secretRef:
name: app-config-v1
When rotating configuration, create app-config-v2, update the Deployment manifest, and roll out:
kubectl create secret generic app-config-v2 --from-literal=api-endpoint=https://api.v2.example.com --immutable
kubectl set env deployment/my-app -n production ENV_VERSION=v2
kubectl rollout status deployment/my-app -n production
If the rollout fails, rollback is instant:
kubectl rollout undo deployment/my-app -n production
The old pods still reference app-config-v1 which remains intact. Clean up old versions after verifying stability:
kubectl delete secret app-config-v1 -n production # Only after confirming no pods reference it
Verification: Check that no pods reference the old Secret:
kubectl get pods -n production -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].envFrom[*].secretRef.name}{"\n"}{end}' | grep app-config-v1
Empty output confirms safe deletion.
Conclusion
Advanced Kubernetes Secret management combines encryption at rest, external secret synchronization, automated rotation, strict RBAC, audit visibility, and immutable versioning to create a defense-in-depth posture. Start by enabling encryption at rest on your control plane — this protects data even if etcd backups are compromised. Layer on the External Secrets Operator or CSI driver to remove secrets from Git and CI pipelines entirely. Enforce least-privilege RBAC with resourceName-scoped Roles, and enable audit logging at the Request level for production namespaces. Finally, adopt immutable, versioned Secrets for configuration changes so rollbacks are instantaneous and auditable. Each layer reduces blast radius and increases operational confidence. Validate each control in a staging cluster before promoting to production, and document the exact commands and expected outputs for your runbooks.