Kubernetes ConfigMaps and Secrets 2025 — Configuration Management Done Right
Advertisement
Introduction
Why This Matters
Hardcoding configuration into container images violates the twelve-factor app methodology and makes multi-environment deployments impossible. A database URL, API key, or feature flag that differs between staging and production cannot be baked into the image — it must be injected at runtime.
Kubernetes provides two resources for this: ConfigMaps for non-sensitive configuration data and Secrets for sensitive data. Both decouple application configuration from the container image, enabling the same image to run in development, staging, and production with different configurations.
However, Kubernetes Secrets are not truly secure out of the box — they are stored as base64-encoded (not encrypted) data in etcd by default. Production teams need to understand this limitation and implement external secret management to address it.
ConfigMaps
ConfigMaps store non-sensitive key-value pairs or entire configuration files.
Creating ConfigMaps
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: production
data:
# Simple key-value pairs
LOG_LEVEL: "info"
PORT: "3000"
NODE_ENV: "production"
CACHE_TTL: "3600"
# Entire config file as a value
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://localhost:3000;
}
}
app-settings.json: |
{
"maxConnections": 100,
"timeout": 30,
"retries": 3
}# Create from YAML
kubectl apply -f configmap.yaml
# Create from literal values
kubectl create configmap api-config \
--from-literal=LOG_LEVEL=info \
--from-literal=PORT=3000 \
-n production
# Create from a file (file name becomes the key)
kubectl create configmap nginx-config \
--from-file=nginx.conf \
-n production
# Create from a directory (all files become keys)
kubectl create configmap app-config \
--from-file=./config/ \
-n production
# View
kubectl get configmap api-config -n production -o yaml
kubectl describe configmap api-config -n productionConsuming ConfigMaps as Environment Variables
spec:
containers:
- name: api
image: myrepo/api:1.0
# Load all keys as environment variables
envFrom:
- configMapRef:
name: api-config
# Or load specific keys
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: api-config
key: LOG_LEVEL
- name: APP_PORT
valueFrom:
configMapKeyRef:
name: api-config
key: PORTConsuming ConfigMaps as Volume Mounts (Files)
spec:
volumes:
- name: nginx-config-vol
configMap:
name: nginx-config
- name: app-settings-vol
configMap:
name: api-config
items:
- key: app-settings.json
path: settings.json # mount as /config/settings.json
containers:
- name: nginx
image: nginx:alpine
volumeMounts:
- name: nginx-config-vol
mountPath: /etc/nginx/conf.d # all keys become files here
readOnly: true
- name: api
image: myrepo/api:1.0
volumeMounts:
- name: app-settings-vol
mountPath: /app/config
readOnly: trueKubernetes automatically updates mounted ConfigMap files when the ConfigMap changes — without restarting the Pod. Environment variables injected via envFrom require a Pod restart to pick up changes.
Secrets
Secrets work like ConfigMaps but are intended for sensitive data. They are stored in etcd and transmitted to nodes only when a Pod on that node requires them.
Creating Secrets
# secret.yaml — use stringData for plain text (auto-encoded to base64)
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
namespace: production
type: Opaque
stringData:
DATABASE_URL: "postgres://app:s3cr3t@db.cluster.svc:5432/mydb"
JWT_SECRET: "my-very-long-random-secret-key-for-jwt-signing"
STRIPE_API_KEY: "sk_live_xxxxxxxxxxxxxxxxxxxxxx"# Create from literal (values are auto-base64-encoded)
kubectl create secret generic api-secrets \
--from-literal=DATABASE_URL='postgres://app:pass@db:5432/mydb' \
--from-literal=JWT_SECRET='supersecretkey' \
-n production
# Create TLS secret (for Ingress TLS)
kubectl create secret tls my-tls-secret \
--cert=tls.crt \
--key=tls.key \
-n production
# Create Docker registry credentials secret
kubectl create secret docker-registry regcred \
--docker-server=123456789.dkr.ecr.us-east-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password --region us-east-1) \
-n production
# View (base64 encoded)
kubectl get secret api-secrets -n production -o yaml
# Decode a specific key
kubectl get secret api-secrets -n production \
-o jsonpath='{.data.DATABASE_URL}' | base64 -dConsuming Secrets in Pods
spec:
containers:
- name: api
image: myrepo/api:1.0
# Load all secret keys as environment variables
envFrom:
- secretRef:
name: api-secrets
# Reference specific secret keys
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: DATABASE_URL
# Reference Docker registry secret for private images
imagePullSecrets:
- name: regcredMounting Secrets as Files
Better than environment variables for secrets — files are not visible in ps aux or crash dumps:
spec:
volumes:
- name: secrets-vol
secret:
secretName: api-secrets
defaultMode: 0400 # read-only by owner
containers:
- name: api
volumeMounts:
- name: secrets-vol
mountPath: /run/secrets
readOnly: true
# Access: cat /run/secrets/DATABASE_URLThe Secret Security Problem
Kubernetes Secrets are base64-encoded, not encrypted. By default, they are stored in plain text in etcd. Anyone with etcd access or the ability to kubectl get secret can read them.
Enable encryption at rest (etcd-level encryption):
# /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: {}External Secrets Operators (Production Standard)
For production, store secrets in dedicated secret managers and sync them to Kubernetes Secrets:
External Secrets Operator (ESO)
# SecretStore points to AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secretsmanager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-credentials
key: access-key-id
secretAccessKeySecretRef:
name: aws-credentials
key: secret-access-key
---
# ExternalSecret syncs from Secrets Manager to K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: SecretStore
target:
name: api-secrets # name of the K8s Secret to create
creationPolicy: Owner
data:
- secretKey: DATABASE_URL # key in K8s Secret
remoteRef:
key: prod/api/database # path in Secrets Manager
property: url
- secretKey: JWT_SECRET
remoteRef:
key: prod/api/jwt
property: secretESO supports AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, and more.
Sealed Secrets (GitOps Approach)
Sealed Secrets encrypt Kubernetes Secrets so they can be committed to Git:
# Install kubeseal CLI
brew install kubeseal
# Seal a secret (encrypted with cluster's public key)
kubectl create secret generic api-secrets \
--from-literal=DATABASE_URL='postgres://...' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
# Commit sealed-secret.yaml to Git safely — only the cluster can decrypt
kubectl apply -f sealed-secret.yamlRBAC for Secret Access
Restrict which service accounts can read Secrets:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["api-secrets"] # specific secret only
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: api-secret-binding
namespace: production
subjects:
- kind: ServiceAccount
name: api-service-account
namespace: production
roleRef:
kind: Role
name: secret-reader
apiGroup: rbac.authorization.k8s.ioCommon Mistakes
- Using Secrets for non-sensitive config (wastes Secret quota and RBAC complexity) — use ConfigMaps for LOG_LEVEL, PORT, etc.
- Assuming Kubernetes Secrets are encrypted — base64 encoding is not encryption; enable etcd encryption or use ESO
- Committing unencrypted secret manifests to Git — use Sealed Secrets or external secret managers
- Using
envFromfor all config including secrets — env vars appear in crash reports andkubectl describe pod - Not setting RBAC restrictions on Secrets — any pod in the namespace can read all secrets by default in many clusters
Best Practices
- Use ConfigMaps for app configuration (log levels, feature flags, ports) and Secrets for credentials and keys
- Mount Secrets as files in
/run/secretsrather than environment variables — files are not captured in crash dumps - Use External Secrets Operator in production to sync from AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault
- Enable etcd encryption at rest on self-managed clusters
- Scope RBAC to specific Secret names, not all secrets in a namespace
- Rotate secrets regularly — ESO's
refreshIntervalenables automatic rotation pickup
Key Takeaways
- ConfigMaps store non-sensitive config as key-value pairs or file contents; Secrets store sensitive data like passwords and tokens
- ConfigMaps mounted as volumes update automatically without pod restart; environment variable ConfigMaps require pod restart
- Kubernetes Secrets are base64-encoded (not encrypted) by default — enable etcd encryption at rest or use external secret managers
- External Secrets Operator syncs from AWS Secrets Manager, GCP Secret Manager, or Vault into Kubernetes Secrets automatically
- Sealed Secrets encrypt secrets so they can be stored safely in Git — the cluster holds the private decryption key
- Mount secrets as files (
/run/secrets/) rather than environment variables to prevent exposure in process listings and crash logs - Use RBAC to restrict secret access to specific ServiceAccounts and specific Secret names, not all secrets in a namespace
- Secret rotation should be automated — manual rotation is a toil that gets skipped under pressure
Advertisement