The Problem with Native Kubernetes Secrets
Kubernetes Secrets are base64-encoded, not encrypted. Anyone with RBAC access to read Secrets in a namespace can decode them instantly:
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
# mysuperpassword123By default, Secrets are stored unencrypted in etcd. This means:
- etcd backups contain plaintext credentials
- Any etcd compromise exposes everything
- GitOps workflows cannot safely store Secret manifests in git
Level 1: Encryption at Rest
The minimum baseline β encrypt Secrets in etcd:
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}Enable on the API server:
# kube-apiserver manifest
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/encryption-config.yamlRe-encrypt existing Secrets:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -Limitation: The encryption key itself must be managed somewhere. If it is on the control plane disk, a node compromise still exposes everything.
Level 2: External Secrets Operator
Pull secrets from external vaults at runtime instead of storing them in Kubernetes:
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespaceAWS Secrets Manager Example
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: prod/database
property: username
- secretKey: password
remoteRef:
key: prod/database
property: passwordHashiCorp Vault Example
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-store
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "my-app"
serviceAccountRef:
name: my-app-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-keys
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-store
kind: SecretStore
target:
name: api-keys
data:
- secretKey: stripe-key
remoteRef:
key: secret/data/payments
property: stripe_api_keyLevel 3: Secrets Store CSI Driver
Mount secrets directly as files without creating Kubernetes Secret objects:
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
-n kube-systemapiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-secrets
spec:
provider: vault
parameters:
vaultAddress: "https://vault.internal:8200"
roleName: "my-app"
objects: |
- objectName: "db-password"
secretPath: "secret/data/database"
secretKey: "password"
---
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.x-k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-secretsThe secret is never stored in etcd β it goes directly from Vault to the pod filesystem.
Level 4: Sealed Secrets for GitOps
When you need Secret manifests in git (ArgoCD, Flux):
# Install Sealed Secrets controller
helm install sealed-secrets sealed-secrets/sealed-secrets \
-n kube-system
# Encrypt a secret
kubectl create secret generic db-creds \
--from-literal=password=supersecret \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-db-creds.yamlapiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-creds
spec:
encryptedData:
password: AgBY7... # Only the cluster can decrypt thisThis is safe to commit to git. Only the clusterβs private key can unseal it.
Secret Rotation Strategy
Automatic Rotation with External Secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rotating-secret
spec:
refreshInterval: 5m # Check for updates every 5 minutes
secretStoreRef:
name: vault-store
kind: SecretStore
target:
name: rotating-secret
creationPolicy: Owner
template:
metadata:
annotations:
secret-rotated: "{{ .creationTimestamp }}"
data:
- secretKey: api-key
remoteRef:
key: secret/data/api
property: keyTriggering Pod Restart on Secret Change
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
annotations:
secret.reloader.stakater.com/reload: "db-credentials,api-keys"
spec:
template:
spec:
containers:
- name: app
envFrom:
- secretRef:
name: db-credentialsUse Reloader to auto-restart pods when secrets change.
RBAC Hardening
Restrict who can read Secrets:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-config"] # Only specific secrets
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-reads-own-secret
namespace: production
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: production
roleRef:
kind: Role
name: app-secret-reader
apiGroup: rbac.authorization.k8s.ioAudit secret access:
# Enable audit logging for secret reads
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets"]Comparison: Which Approach When?
| Approach | GitOps Safe | No etcd Storage | Auto-Rotation | Complexity |
|---|---|---|---|---|
| Native + encryption at rest | β | β | β | Low |
| Sealed Secrets | β | β | β | Medium |
| External Secrets Operator | β | β (creates K8s Secret) | β | Medium |
| CSI Driver | N/A | β | β | High |
| Vault Agent Sidecar | N/A | β | β | High |
My Recommendation
For most production clusters in 2026:
- External Secrets Operator as the primary mechanism (works with any vault)
- Sealed Secrets for bootstrap secrets that must exist before the operator runs
- Encryption at rest as a defense-in-depth layer
- Reloader for automatic pod restarts on rotation
- RBAC + audit logging to track who accesses what
Start simple, add layers as your threat model requires.
Deepen Your Kubernetes Skills
If you found this article useful, check out my books for hands-on Kubernetes mastery:
- Kubernetes Recipes β A practical guide for container orchestration and deployment with real-world patterns
- Ansible for Kubernetes by Example β Automate Kubernetes cluster operations with Ansible playbooks
Both books follow the same practical, example-driven approach you see in my articles.