Kubernetes ConfigMaps and Secrets: Internals, Pitfalls, and Production Patterns
- ConfigMaps and Secrets decouple configuration from container images, but they have fundamentally different security profiles.
- Base64 is not encryption. Enable envelope encryption for Secrets at rest. Use external secret stores for mature environments.
- Volume mounts support live rotation; env vars do not. Design your application to re-read mounted files.
- ConfigMap: Stores non-sensitive key-value pairs (feature flags, URLs, config files).
- Secret: Stores sensitive data (passwords, tokens, TLS certs). Values are base64-encoded, NOT encrypted by default.
- Both can be injected as environment variables OR mounted as files into a pod.
- Storage: Both are stored in etcd. Secrets have an additional encryption-at-rest layer (envelope encryption) that must be explicitly enabled.
- Env vars: Simple, but require pod restart for updates. Visible in pod spec and process environment.
- Volume mounts: Support live rotation without restart. File permissions can be restricted.
- Assuming base64 encoding equals encryption. It does not. Anyone with API access can decode a Secret. Envelope encryption must be configured separately via EncryptionConfiguration.
Pod cannot find a ConfigMap or Secret.
kubectl get configmap <name> -n <namespace>kubectl get secret <name> -n <namespace> -o yamlSecret value appears wrong or empty.
kubectl get secret <name> -o jsonpath='{.data.<key>}' | base64 -dkubectl describe secret <name>Mounted ConfigMap file not updating after ConfigMap change.
kubectl exec <pod> -- cat /path/to/configkubectl get configmap <name> -o yaml | grep -A 5 dataApplication logs show 'access denied' to Secret file.
kubectl exec <pod> -- ls -la /path/to/secretkubectl get pod <pod> -o jsonpath='{.spec.securityContext}'Production Incident
kubectl rollout restart deployment with maxUnavailable: 1 to avoid simultaneous restarts.
4. Configured the application's connection pool to retry authentication with exponential backoff.Production Debug GuideSymptom-first investigation path for configuration and credential failures in Kubernetes.
kubectl describe pod output.→This is expected for env-var-injected Secrets. The value is base64-decoded and shown in the pod spec. Switch to volume mount to limit exposure. Review RBAC to restrict who can describe pods.defaultMode field in the volume mount. Verify the pod's securityContext.runAsUser can read the file. Default mode is 0644 but can be overridden.kubectl get secret <name> -o json | wc -c to measure size. Consider external secret stores for large payloads.Every production Kubernetes cluster eventually hits the same wall: the app image is beautifully immutable, but the configuration it needs — database URLs, feature flags, TLS certificates, API keys — changes constantly across environments. Baking config into the image means rebuilding for a one-line change. Passing it ad-hoc at runtime means no audit trail and no consistency across hundreds of pods.
Kubernetes solves this with two first-class API objects: ConfigMaps for ordinary configuration data and Secrets for sensitive credentials. Both decouple config from the container image, but they have fundamentally different storage mechanisms, access controls, and risk profiles. Understanding that difference — not just syntactically, but at the etcd and kubelet level — is what separates engineers who use these safely from engineers who accidentally expose credentials in pod specs.
This is not a syntax reference. It is for engineers who need to understand how ConfigMaps and Secrets are persisted, why base64 is NOT encryption, when to mount as a file versus inject as an environment variable and why it matters for rotation, how to enable envelope encryption at rest, and the RBAC patterns that keep least-privilege real in a multi-team cluster.
What is Kubernetes ConfigMaps and Secrets?
Kubernetes ConfigMaps and Secrets are core API objects that externalize configuration from container images. ConfigMaps hold non-sensitive data: environment-specific URLs, feature flags, configuration files. Secrets hold sensitive data: passwords, API tokens, TLS private keys. Both are namespaced resources, stored in etcd, and can be consumed by pods as environment variables or mounted volumes.
# ConfigMap: Non-sensitive configuration # Package: io.thecodeforge.kubernetes apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: production data: DATABASE_HOST: "postgres.internal.svc.cluster.local" LOG_LEVEL: "info" FEATURE_FLAG_NEW_UI: "true" binaryData: logo.png: "iVBORw0KGgoAAAANSUhEUg..." # binary data supported --- # Secret: Sensitive credentials apiVersion: v1 kind: Secret metadata: name: db-credentials namespace: production type: Opaque # default type data: username: cG9zdGdyZXM= # base64-encoded 'postgres' password: c3VwZXJzZWNyZXQ= # base64-encoded 'supersecret' stringData: connection-string: "postgresql://postgres:supersecret@postgres:5432/appdb" # plain text, auto-encoded
- base64 is reversible with a single command:
echo <value> | base64 -d. - Security relies on RBAC: who can
getorlistSecrets. - Encryption at rest (envelope encryption) protects against etcd data theft, not API access.
- For real encryption, use external secret stores (Vault, AWS Secrets Manager) with the Secrets Store CSI Driver.
get permission on the secrets resource in that namespace. In practice, this means: cluster admins can see all secrets, CI/CD service accounts often have broad secret access, and compromised pods with mounted service account tokens can potentially read other secrets. Mitigate with: dedicated service accounts per workload, minimal RBAC roles, and disabling automounting of service account tokens (automountServiceAccountToken: false).Storage Internals: How etcd Handles ConfigMaps and Secrets
Both ConfigMaps and Secrets are stored as key-value pairs in etcd through the API Server. The API Server is the only component that talks to etcd directly. When you create a ConfigMap or Secret, the API Server validates it, serializes it, and writes it to etcd. When a kubelet needs to mount a Secret into a pod, it fetches the data from the API Server (which reads from etcd).
# Envelope Encryption Configuration for Secrets at Rest # This file goes on the API Server as --encryption-provider-config # Package: io.thecodeforge.kubernetes apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets # Encrypt Secrets at rest - configmaps # Optionally encrypt ConfigMaps too providers: - aescbc: # Local encryption (AES-CBC) keys: - name: key1 secret: <base64-encoded-32-byte-key> - identity: {} # Fallback: no encryption (reads old unencrypted data)
- Default: Secrets are stored in plain text in etcd.
- Envelope encryption is opt-in via
--encryption-provider-configflag on the API Server. - Key rotation: Add a new key to the config, restart API Server, then run
kubectl get secrets -A -o json | kubectl replace -f -to re-encrypt. - Cloud KMS integration (e.g., AWS KMS) means you never handle the KEK directly.
etcd_debugging_mvcc_db_total_size_in_bytes metric; large Secrets (e.g., multi-megabyte TLS certs) inflate etcd and degrade performance.Env Var vs Volume Mount: The Rotation Trade-off
ConfigMaps and Secrets can be consumed by pods in two ways: as environment variables or as mounted files. This choice has significant implications for secret rotation, visibility, and application architecture.
# Pattern 1: Environment Variable Injection (Static) # Values are set at pod creation time. Do NOT update on Secret change. apiVersion: v1 kind: Pod metadata: name: app-env-injection spec: containers: - name: app image: io.thecodeforge/app:1.0 envFrom: - configMapRef: name: app-config - secretRef: name: db-credentials env: - name: SPECIFIC_KEY valueFrom: secretKeyRef: name: db-credentials key: password --- # Pattern 2: Volume Mount (Dynamic Rotation Support) # Files update when source Secret/ConfigMap changes (kubelet sync interval). apiVersion: v1 kind: Pod metadata: name: app-volume-mount spec: containers: - name: app image: io.thecodeforge/app:1.0 volumeMounts: - name: config-volume mountPath: /etc/config readOnly: true - name: secret-volume mountPath: /etc/secrets readOnly: true # subPath mounts do NOT auto-update! # subPath example (static, no rotation): # - name: secret-volume # mountPath: /etc/secrets/password.txt # subPath: password volumes: - name: config-volume configMap: name: app-config - name: secret-volume secret: secretName: db-credentials defaultMode: 0440 # read-only for owner and group
- Env vars: Simple, but require pod restart for updates. Values visible in
kubectl describe pod. - Volume mounts: Support live rotation (~60s kubelet sync). Values NOT visible in pod spec.
- subPath mounts: Do NOT auto-update. Avoid for rotating secrets.
- Application must watch or periodically re-read mounted files to detect changes.
--sync-frequency (default 1 minute). This means there is up to a 60-second window where a pod has stale credentials after a Secret update. For security-critical rotations (e.g., revoking a compromised key), this delay is unacceptable. In those cases, perform a rolling pod restart immediately after updating the Secret, rather than relying on the kubelet sync.RBAC and Least Privilege for Secrets
Secret access must be tightly controlled via RBAC. In a multi-team cluster, the default behavior — where any pod's service account can potentially read any Secret in its namespace — is a security anti-pattern.
# Service Account: Dedicated per workload apiVersion: v1 kind: ServiceAccount metadata: name: payment-service-account namespace: payments automountServiceAccountToken: false # Disable unless the app needs API access --- # Role: Read only specific Secrets apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: secret-reader namespace: payments rules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["payment-gateway-key", "db-credentials"] # Only specific secrets verbs: ["get"] # NOT 'list' — prevents enumeration --- # RoleBinding: Bind SA to Role apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: payment-secret-access namespace: payments subjects: - kind: ServiceAccount name: payment-service-account namespace: payments roleRef: kind: Role name: secret-reader apiGroup: rbac.authorization.k8s.io
automountServiceAccountToken: false should be your default unless the pod explicitly needs Kubernetes API access.- Default service account has broad permissions in many clusters.
- Compromised pod + mounted token = potential Secret enumeration.
- Set
automountServiceAccountToken: falseon all pods that do not need API access. - Use
resourceNamesin RBAC to restrict which specific Secrets a service account can read. - Avoid
listverb on Secrets — it allows enumeration of all Secret names.
automountServiceAccountToken: false as a cluster-wide default. Audit RBAC regularly: kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa> shows exactly what a service account can do.list on Secrets unless absolutely necessary. Treat every service account token as a potential attack vector.External Secret Stores: Beyond Native Kubernetes Secrets
For organizations with mature secret management practices, native Kubernetes Secrets are often insufficient. External secret stores like HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, and Azure Key Vault provide centralized management, automatic rotation, audit logging, and fine-grained access policies that Kubernetes RBAC alone cannot match.
# Secrets Store CSI Driver: Mount external secrets as volumes # Requires: secrets-store-csi-driver installed in cluster apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: vault-db-creds namespace: production spec: provider: vault # or aws, gcp, azure parameters: vaultAddress: "https://vault.internal:8200" roleName: "payment-service" objects: | - objectName: "db-password" secretPath: "secret/data/production/db" secretKey: "password" --- # Pod consuming the external secret apiVersion: v1 kind: Pod metadata: name: payment-service spec: containers: - name: app image: io.thecodeforge/payment-service:2.1 volumeMounts: - name: secrets-store mountPath: /mnt/secrets readOnly: true volumes: - name: secrets-store csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: "vault-db-creds"
- Native Secrets: Simple, no external dependencies, but limited rotation and audit capabilities.
- External stores: Centralized lifecycle, audit logs, automatic rotation, cross-cluster sharing.
- Secrets Store CSI Driver: The bridge. Mounts external secrets as files without storing them in etcd.
- External Secrets Operator: Alternative approach. Syncs external secrets INTO native Kubernetes Secrets.
--set syncSecret.enabled=true), and fallback logic in your application.| Aspect | ConfigMap | Secret |
|---|---|---|
| Purpose | Non-sensitive configuration data | Sensitive credentials, keys, tokens |
| Encoding | Plain text (UTF-8) | Base64-encoded (NOT encrypted by default) |
| Size Limit | 1 MiB per ConfigMap | 1 MiB per Secret |
| Encryption at Rest | Not encrypted by default (can be enabled) | Not encrypted by default (envelope encryption opt-in) |
| Env Var Injection | Supported (visible in pod spec) | Supported (values visible in kubectl describe pod) |
| Volume Mount | Supported (auto-updates on change) | Supported (auto-updates on change) |
| subPath Mount | Static (no auto-update) | Static (no auto-update) |
| RBAC Consideration | Generally low-risk to expose | Critical: restrict get and list verbs |
| Use Case | Feature flags, URLs, config files | Passwords, API keys, TLS certs, tokens |
🎯 Key Takeaways
- ConfigMaps and Secrets decouple configuration from container images, but they have fundamentally different security profiles.
- Base64 is not encryption. Enable envelope encryption for Secrets at rest. Use external secret stores for mature environments.
- Volume mounts support live rotation; env vars do not. Design your application to re-read mounted files.
- RBAC is the real security boundary. Use dedicated service accounts, restrict to specific Secret names, and disable automounting.
- Never use subPath for rotating secrets. Never grant
liston Secrets unless absolutely required. - Test your rotation runbook. The gap between 'Secret updated' and 'pod using new credentials' is where outages happen.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the difference between ConfigMaps and Secrets at the storage level. Where are they persisted and what are the security implications?
- QA team injects database credentials as environment variables. What happens when they rotate the Secret? How would you change the architecture?
- QWhat is envelope encryption in Kubernetes and why does it matter for Secrets?
- QHow does the kubelet update mounted ConfigMaps and Secrets? What is the sync interval and what are the edge cases?
- QDescribe a least-privilege RBAC setup for Secret access in a multi-team cluster.
- QWhen would you recommend an external secret store (e.g., Vault) over native Kubernetes Secrets? What are the trade-offs?
- QWhat is the
subPathproblem with Secret mounts and how do you avoid it? - QHow would you design a zero-downtime Secret rotation workflow?
Frequently Asked Questions
What is the difference between ConfigMaps and Secrets in Kubernetes?
ConfigMaps store non-sensitive configuration data (plain text). Secrets store sensitive data (base64-encoded by default, with optional envelope encryption at rest). Both can be injected as environment variables or mounted as volume files. The key difference is security: Secrets have additional RBAC and encryption considerations.
Is base64 encoding in Secrets secure?
No. Base64 is a serialization format, not encryption. Anyone with read access to the Secret resource can decode the values. Security comes from RBAC (who can access the Secret) and optionally from envelope encryption (protecting the data at rest in etcd).
How do I rotate a Secret without downtime?
Mount the Secret as a volume (not an env var). The kubelet will sync the updated file within ~60 seconds. Design your application to re-read the file. For critical rotations, perform a rolling restart of pods after updating the Secret, using kubectl rollout restart deployment with controlled maxUnavailable.
What is envelope encryption and should I enable it?
Envelope encryption encrypts Secret values before writing to etcd, using a local Data Encryption Key (DEK) which is itself encrypted by a Key Encryption Key (KEK). Yes, enable it in production. Without it, Secrets are stored in plain text in etcd. Use a cloud KMS (AWS KMS, GCP KMS) as the KEK provider for automated key management.
When should I use an external secret store instead of native Kubernetes Secrets?
Use external stores when you need: centralized secret management across clusters, automatic rotation policies, audit logging for compliance, or secrets shared across cloud accounts. The Secrets Store CSI Driver or External Secrets Operator are the integration layers. The trade-off is operational complexity and a runtime dependency on the external store.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.