Managing Kubernetes Secrets with Vault and ConfigMaps
Overview and What You Will Learn
Hardcoding credentials inside Docker images or environment variables is one of the most dangerous and common security mistakes in Kubernetes deployments. This lab walks you through the correct production approach β managing sensitive credentials using Kubernetes Secrets with encryption at rest, injecting secrets dynamically using HashiCorp Vault, and separating non-sensitive configuration using ConfigMaps with proper RBAC controls.
By the end of this guide you will be able to:
- Create and consume Kubernetes Secrets safely as environment variables and mounted files
- Enable encryption at rest for Secrets stored in etcd
- Deploy HashiCorp Vault on Kubernetes and inject secrets dynamically into pods
- Use ConfigMaps for non-sensitive application configuration with automatic reload
- Apply RBAC policies to restrict which pods and service accounts can access which secrets
Why This Matters in Production
In 2023, a major Indian fintech platform suffered a credential leak because database passwords were stored as plain environment variables in their Kubernetes Deployment YAML files β which were committed to a public GitHub repository. The blast radius included customer PII exposure and regulatory penalties.
At Zerodha, Razorpay, and PhonePe β platforms handling real financial transactions β secrets management is a compliance requirement, not just a best practice. A single leaked database credential or API key can compromise millions of user accounts. Every engineer deploying to Kubernetes must understand how to handle credentials correctly from day one.
Core Principles
The secrets management hierarchy in a production Kubernetes cluster:
Application needs a credential (e.g., DB password, API key) β βββΊ Non-sensitive config (LOG_LEVEL, APP_ENV, PORT) β βββΊ ConfigMap β injected as env vars or mounted files β βββΊ Sensitive credentials (DB_PASSWORD, API_KEY, JWT_SECRET) β βββΊ Basic: Kubernetes Secret (base64, encrypted at rest) β βββΊ Acceptable for small teams with RBAC controls β βββΊ Advanced: HashiCorp Vault (dynamic secrets, audit logs) βββΊ Required for compliance-grade production systemsKey rules every engineer must follow:
- Never store sensitive values in ConfigMaps β they are plain text in etcd
- Never commit Secret YAML files to Git β use Sealed Secrets or Vault instead
- Always enable encryption at rest for the Secrets API group in etcd
- Always use RBAC to restrict which service accounts can read which secrets
- Rotate secrets regularly β Vault enables automatic rotation without pod restarts
Detailed Step-by-Step Practical Lab
Step 1 β Create and Use Basic Kubernetes Secrets
1# Create a secret from literal values β never store in YAML files2kubectl create secret generic db-credentials \3 --from-literal=DB_HOST=10.0.1.50 \4 --from-literal=DB_NAME=zerodha_trading \5 --from-literal=DB_USER=rahul \6 --from-literal=DB_PASSWORD=Tr@d3Secure#9182 \7 -n production8 9# Verify the secret was created10kubectl get secret db-credentials -n production11 12# Inspect what keys are stored (values are base64 encoded)13kubectl describe secret db-credentials -n production14 15# Decode a specific value to verify16kubectl get secret db-credentials -n production \17 -o jsonpath='{.data.DB_PASSWORD}' | base64 --decodeStep 2 β Inject Secrets into Pods as Environment Variables
1# deployment.yaml β inject secrets as env vars into the trading API2apiVersion: apps/v13kind: Deployment4metadata:5 name: trading-api6 namespace: production7spec:8 replicas: 39 selector:10 matchLabels:11 app: trading-api12 template:13 metadata:14 labels:15 app: trading-api16 spec:17 serviceAccountName: trading-api-sa # Dedicated service account with RBAC18 containers:19 - name: trading-api20 image: registry.zerodha.in/trading-api:v4.2.121 envFrom:22 - configMapRef:23 name: app-config # Non-sensitive config24 env:25 - name: DB_HOST26 valueFrom:27 secretKeyRef:28 name: db-credentials29 key: DB_HOST30 - name: DB_PASSWORD31 valueFrom:32 secretKeyRef:33 name: db-credentials34 key: DB_PASSWORD35 - name: JWT_SECRET36 valueFrom:37 secretKeyRef:38 name: jwt-credentials39 key: JWT_SECRETStep 3 β Mount Secrets as Files for Certificate and Key Management
1# Mount TLS certificate and private key as files inside the container2spec:3 containers:4 - name: trading-api5 image: registry.zerodha.in/trading-api:v4.2.16 volumeMounts:7 - name: tls-certs8 mountPath: /etc/ssl/certs/app # Application reads certs from here9 readOnly: true10 volumes:11 - name: tls-certs12 secret:13 secretName: zerodha-tls-secret # Contains tls.crt and tls.key14 defaultMode: 0400 # Read-only for owner only β criticalβ οΈ Security: Always set defaultMode: 0400 on secret volume mounts. The default mode 0644 makes private keys readable by all users on the container β a significant security risk.Step 4 β Enable Encryption at Rest for Secrets in etcd
By default, Kubernetes Secrets are stored as base64 in etcd β not encrypted. Anyone with etcd access can read all secrets. Fix this:
1# encryption-config.yaml β place on the control plane node2# Path: /etc/kubernetes/encryption-config.yaml3apiVersion: apiserver.config.k8s.io/v14kind: EncryptionConfiguration5resources:6 - resources:7 - secrets8 providers:9 - aescbc: # AES-CBC encryption10 keys:11 - name: key112 secret: c2VjcmV0LWtleS0zMi1ieXRlcy1sb25nLWhlcmU= # base64 of 32-byte key13 - identity: {} # Fallback for reading unencrypted secrets1# Generate a proper 32-byte encryption key2head -c 32 /dev/urandom | base643 4# Apply to kube-apiserver by adding this flag to the apiserver manifest5# /etc/kubernetes/manifests/kube-apiserver.yaml6# --encryption-provider-config=/etc/kubernetes/encryption-config.yaml7 8# Verify all existing secrets are now encrypted by rewriting them9kubectl get secrets --all-namespaces -o json | kubectl replace -f -π Remember: Encryption at rest only protects secrets from direct etcd database access. It does not protect secrets from being read via the Kubernetes API by authorised users. RBAC controls who can read secrets via the API.
Step 5 β Deploy HashiCorp Vault on Kubernetes
1# Add the HashiCorp Helm repository2helm repo add hashicorp https://helm.releases.hashicorp.com3helm repo update4 5# Install Vault in HA mode with integrated storage6helm install vault hashicorp/vault \7 --namespace vault \8 --create-namespace \9 --set server.ha.enabled=true \10 --set server.ha.replicas=3 \11 --set server.dataStorage.size=10Gi12 13# Verify Vault pods are running14kubectl get pods -n vault15# NAME READY STATUS RESTARTS16# vault-0 0/1 Running 0 β Not ready until initialized17# vault-1 0/1 Running 018# vault-2 0/1 Running 019 20# Initialize Vault β generates unseal keys and root token21kubectl exec -n vault vault-0 -- vault operator init \22 -key-shares=5 \23 -key-threshold=3 \24 -format=json > vault-init-keys.json25 26# CRITICAL: Store vault-init-keys.json securely β losing it = losing all secretsβ οΈ Security: Never store vault-init-keys.json on the same cluster or any version control system. Store unseal keys in separate secure locations β ideally split across different team members using Shamir's Secret Sharing.Step 6 β Configure Vault Agent Injector for Automatic Secret Injection
1# Enable Kubernetes authentication in Vault2kubectl exec -n vault vault-0 -- vault auth enable kubernetes3 4# Configure Vault to trust your cluster's service accounts5kubectl exec -n vault vault-0 -- vault write auth/kubernetes/config \6 kubernetes_host="https://kubernetes.default.svc.cluster.local:443"7 8# Write a secret into Vault9kubectl exec -n vault vault-0 -- vault kv put secret/production/trading-api \10 db_password="Tr@d3Secure#9182" \11 api_key="rzp_live_xK9mN3pQ7wL2aB" \12 jwt_secret="HS256-prod-secret-zerodha-2024"13 14# Create a Vault policy allowing read access to this path15kubectl exec -n vault vault-0 -- vault policy write trading-api-policy - <<EOF16path "secret/data/production/trading-api" {17 capabilities = ["read"]18}19EOF20 21# Bind the Kubernetes service account to the Vault policy22kubectl exec -n vault vault-0 -- vault write \23 auth/kubernetes/role/trading-api \24 bound_service_account_names=trading-api-sa \25 bound_service_account_namespaces=production \26 policies=trading-api-policy \27 ttl=1h1# deployment-vault.yaml β pod with Vault Agent sidecar injection2apiVersion: apps/v13kind: Deployment4metadata:5 name: trading-api6 namespace: production7spec:8 template:9 metadata:10 labels:11 app: trading-api12 annotations:13 vault.hashicorp.com/agent-inject: "true" # Enable Vault sidecar14 vault.hashicorp.com/role: "trading-api" # Vault role to use15 vault.hashicorp.com/agent-inject-secret-config: "secret/data/production/trading-api"16 vault.hashicorp.com/agent-inject-template-config: | # Template the secret as a file17 {{- with secret "secret/data/production/trading-api" -}}18 export DB_PASSWORD="{{ .Data.data.db_password }}"19 export API_KEY="{{ .Data.data.api_key }}"20 export JWT_SECRET="{{ .Data.data.jwt_secret }}"21 {{- end }}22 spec:23 serviceAccountName: trading-api-sa24 containers:25 - name: trading-api26 image: registry.zerodha.in/trading-api:v4.2.127 command: ["/bin/sh", "-c"]28 args: ["source /vault/secrets/config && node server.js"]Step 7 β Apply RBAC to Restrict Secret Access
1# rbac-secrets.yaml β only the trading-api service account can read db-credentials2apiVersion: v13kind: ServiceAccount4metadata:5 name: trading-api-sa6 namespace: production7apiVersion: rbac.authorization.k8s.io/v18kind: RoleBinding9metadata:10 name: trading-api-secret-binding11 namespace: production12subjects:13 - kind: ServiceAccount14 name: trading-api-sa15 namespace: production16roleRef:17 kind: Role18 apiVersion: rbac.authorization.k8s.io/v119 name: trading-api-secret-reader1kubectl apply -f rbac-secrets.yaml2 3# Verify the service account can only access its designated secrets4kubectl auth can-i get secret/db-credentials \5 --as=system:serviceaccount:production:trading-api-sa \6 -n production7# yes8 9kubectl auth can-i list secrets \10 --as=system:serviceaccount:production:trading-api-sa \11 -n production12# noπ‘ Tip: Always use resourceNames in your RBAC Role to restrict access to specific named secrets. Without it, the service account can read ALL secrets in the namespace β including other teams' credentials.Production Best Practices & Common Pitfalls
- Use External Secrets Operator as an alternative to Vault for teams already using AWS Secrets Manager or GCP Secret Manager β it syncs external secrets into Kubernetes Secrets automatically.
- Never use
kubectl get secret -o yamlon a production terminal that is being screen-recorded or shared. The base64 values are trivially decodable. - Rotate all secrets after any team member leaves the organisation. Vault makes this seamless with dynamic secrets that auto-expire.
- Use Sealed Secrets (by Bitnami) if you must commit secret manifests to Git β it encrypts secrets with a cluster-specific key so the YAML is safe to store in version control.
- Audit secret access regularly using Kubernetes audit logs β any unexpected
get secretevent from an unrecognised service account is a red flag.
π΄ Common Mistake: UsingenvFrom: secretRefto inject an entire secret as environment variables. This injects ALL keys including ones the application does not need, violating the principle of least privilege. Always inject only the specific keys your application requires usingsecretKeyRef.
Quick Reference & Troubleshooting Commands
| Command | Purpose |
|---|---|
kubectl create secret generic <name> --from-literal=KEY=VALUE -n <ns> |
Create a secret from literal values |
kubectl get secret <name> -n <ns> -o jsonpath='{.data.KEY}' | base64 --decode |
Decode a specific secret value |
kubectl describe secret <name> -n <ns> |
List secret keys without revealing values |
kubectl delete secret <name> -n <ns> |
Delete a secret |
kubectl auth can-i get secret/<name> --as=system:serviceaccount:<ns>:<sa> -n <ns> |
Test RBAC access for a service account |
vault kv get secret/production/trading-api |
Read a secret from Vault |
vault kv put secret/production/trading-api key=value |
Write a secret to Vault |
vault lease revoke -prefix secret/production/ |
Revoke all dynamic secrets under a path |
kubectl get externalsecrets -n <ns> |
List External Secrets Operator synced secrets |