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
# Create a secret from literal values — never store in YAML fileskubectl create secret generic db-credentials \ --from-literal=DB_HOST=10.0.1.50 \ --from-literal=DB_NAME=zerodha_trading \ --from-literal=DB_USER=rahul \ --from-literal=DB_PASSWORD=Tr@d3Secure#9182 \ -n production # Verify the secret was createdkubectl get secret db-credentials -n production # Inspect what keys are stored (values are base64 encoded)kubectl describe secret db-credentials -n production # Decode a specific value to verifykubectl get secret db-credentials -n production \ -o jsonpath='{.data.DB_PASSWORD}' | base64 --decodeStep 2 — Inject Secrets into Pods as Environment Variables
# deployment.yaml — inject secrets as env vars into the trading APIapiVersion: apps/v1kind: Deploymentmetadata: name: trading-api namespace: productionspec: replicas: 3 selector: matchLabels: app: trading-api template: metadata: labels: app: trading-api spec: serviceAccountName: trading-api-sa # Dedicated service account with RBAC containers: - name: trading-api image: registry.zerodha.in/trading-api:v4.2.1 envFrom: - configMapRef: name: app-config # Non-sensitive config env: - name: DB_HOST valueFrom: secretKeyRef: name: db-credentials key: DB_HOST - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: DB_PASSWORD - name: JWT_SECRET valueFrom: secretKeyRef: name: jwt-credentials key: JWT_SECRETStep 3 — Mount Secrets as Files for Certificate and Key Management
# Mount TLS certificate and private key as files inside the containerspec: containers: - name: trading-api image: registry.zerodha.in/trading-api:v4.2.1 volumeMounts: - name: tls-certs mountPath: /etc/ssl/certs/app # Application reads certs from here readOnly: true volumes: - name: tls-certs secret: secretName: zerodha-tls-secret # Contains tls.crt and tls.key defaultMode: 0400 # Read-only for owner only — criticalCOMMON MISTAKE / WARNING**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:
# encryption-config.yaml — place on the control plane node# Path: /etc/kubernetes/encryption-config.yamlapiVersion: apiserver.config.k8s.io/v1kind: EncryptionConfigurationresources: - resources: - secrets providers: - aescbc: # AES-CBC encryption keys: - name: key1 secret: c2VjcmV0LWtleS0zMi1ieXRlcy1sb25nLWhlcmU= # base64 of 32-byte key - identity: {} # Fallback for reading unencrypted secrets# Generate a proper 32-byte encryption keyhead -c 32 /dev/urandom | base64 # Apply to kube-apiserver by adding this flag to the apiserver manifest# /etc/kubernetes/manifests/kube-apiserver.yaml# --encryption-provider-config=/etc/kubernetes/encryption-config.yaml # Verify all existing secrets are now encrypted by rewriting themkubectl get secrets --all-namespaces -o json | kubectl replace -f -REMEMBER THIS**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
# Add the HashiCorp Helm repositoryhelm repo add hashicorp https://helm.releases.hashicorp.comhelm repo update # Install Vault in HA mode with integrated storagehelm install vault hashicorp/vault \ --namespace vault \ --create-namespace \ --set server.ha.enabled=true \ --set server.ha.replicas=3 \ --set server.dataStorage.size=10Gi # Verify Vault pods are runningkubectl get pods -n vault# NAME READY STATUS RESTARTS# vault-0 0/1 Running 0 ← Not ready until initialized# vault-1 0/1 Running 0# vault-2 0/1 Running 0 # Initialize Vault — generates unseal keys and root tokenkubectl exec -n vault vault-0 -- vault operator init \ -key-shares=5 \ -key-threshold=3 \ -format=json > vault-init-keys.json # CRITICAL: Store vault-init-keys.json securely — losing it = losing all secretsCOMMON MISTAKE / WARNING**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
# Enable Kubernetes authentication in Vaultkubectl exec -n vault vault-0 -- vault auth enable kubernetes # Configure Vault to trust your cluster's service accountskubectl exec -n vault vault-0 -- vault write auth/kubernetes/config \ kubernetes_host="https://kubernetes.default.svc.cluster.local:443" # Write a secret into Vaultkubectl exec -n vault vault-0 -- vault kv put secret/production/trading-api \ db_password="Tr@d3Secure#9182" \ api_key="rzp_live_xK9mN3pQ7wL2aB" \ jwt_secret="HS256-prod-secret-zerodha-2024" # Create a Vault policy allowing read access to this pathkubectl exec -n vault vault-0 -- vault policy write trading-api-policy - <<EOFpath "secret/data/production/trading-api" { capabilities = ["read"]}EOF # Bind the Kubernetes service account to the Vault policykubectl exec -n vault vault-0 -- vault write \ auth/kubernetes/role/trading-api \ bound_service_account_names=trading-api-sa \ bound_service_account_namespaces=production \ policies=trading-api-policy \ ttl=1h# deployment-vault.yaml — pod with Vault Agent sidecar injectionapiVersion: apps/v1kind: Deploymentmetadata: name: trading-api namespace: productionspec: template: metadata: labels: app: trading-api annotations: vault.hashicorp.com/agent-inject: "true" # Enable Vault sidecar vault.hashicorp.com/role: "trading-api" # Vault role to use vault.hashicorp.com/agent-inject-secret-config: "secret/data/production/trading-api" vault.hashicorp.com/agent-inject-template-config: | # Template the secret as a file {{- with secret "secret/data/production/trading-api" -}} export DB_PASSWORD="{{ .Data.data.db_password }}" export API_KEY="{{ .Data.data.api_key }}" export JWT_SECRET="{{ .Data.data.jwt_secret }}" {{- end }} spec: serviceAccountName: trading-api-sa containers: - name: trading-api image: registry.zerodha.in/trading-api:v4.2.1 command: ["/bin/sh", "-c"] args: ["source /vault/secrets/config && node server.js"]Step 7 — Apply RBAC to Restrict Secret Access
# rbac-secrets.yaml — only the trading-api service account can read db-credentialsapiVersion: v1kind: ServiceAccountmetadata: name: trading-api-sa namespace: productionapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: trading-api-secret-binding namespace: productionsubjects: - kind: ServiceAccount name: trading-api-sa namespace: productionroleRef: kind: Role apiVersion: rbac.authorization.k8s.io/v1 name: trading-api-secret-readerkubectl apply -f rbac-secrets.yaml # Verify the service account can only access its designated secretskubectl auth can-i get secret/db-credentials \ --as=system:serviceaccount:production:trading-api-sa \ -n production# yes kubectl auth can-i list secrets \ --as=system:serviceaccount:production:trading-api-sa \ -n production# noPLACEMENT PRO TIP**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 / WARNING**Common Mistake:** Using `envFrom: secretRef` to 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 using `secretKeyRef`.
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 |