You hardcoded a database password in your image. A week later, someone forks the repo and the credential ships to Docker Hub. Or you baked environment-specific URLs into your application code, and now every environment needs a different image. Both problems have the same solution: ConfigMaps and Secrets — Kubernetes-native objects that separate configuration from containers.
This guide covers what each object is for, how to create and inject them, the differences that actually matter in production, and the mistakes that leak credentials or break rolling updates.
1. ConfigMap vs Secret: What's the Difference?
Both store key-value data and both get injected into pods the same way. The differences are operational:
| Attribute | ConfigMap | Secret |
|---|---|---|
| Stores | Non-sensitive config (URLs, feature flags, app settings) | Sensitive data (passwords, API keys, TLS certs) |
| Value encoding | Plain text | Base64-encoded (encoding, not encryption) |
| Storage | etcd, readable by anyone with API access | etcd — enable encryption at rest for real protection |
| Consumption | Env vars, volumes, CLI args | Env vars, volumes, image pull credentials |
| Rotation | Recreate pods to pick up changes | Recreate pods; mount updates only via volume |
The rule of thumb: if you wouldn't paste it into a public README, it belongs in a Secret. Everything else goes in a ConfigMap.
2. Creating ConfigMaps
You can create a ConfigMap from literals, files, or a manifest:
# From literals
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=MAX_CONNECTIONS=100
# From a file (key = filename)
kubectl create configmap nginx-config --from-file=nginx.conf
# From a whole directory
kubectl create configmap app-config --from-file=./configs/
The declarative version is better for GitOps workflows:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
app.properties: |
cache.ttl=300
retry.max=3
Note that values in data are always strings. Numbers and booleans must be quoted, and multi-line content is handled with the | block scalar.
3. Injecting Config into Pods
There are two main injection paths, and they behave differently on update.
Environment variables
containers:
- name: app
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
envFrom pulls in every key as an environment variable — fast to set up, but it silently fails on invalid keys and can collide with existing env vars. configMapKeyRef is explicit and gives you a specific variable. Use envFrom for teams, explicit refs for critical values.
Volume mounts
volumes:
- name: config-volume
configMap:
name: app-config
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: config-volume
mountPath: /etc/config
Volume mounts are where the behavior difference matters: volume-mounted config updates in place when the ConfigMap changes (kubelet syncs it every ~60 seconds). Environment variables are fixed at pod start — updating the ConfigMap does nothing until you recreate the pod.
This is the classic source of "I changed the ConfigMap but my app still uses the old value" confusion. If you use env vars, you must trigger a rollout: kubectl rollout restart deployment/app.
4. Working with Secrets
Secrets follow the same patterns, with extra care around how values are written:
# From literals — kubectl base64-encodes for you
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='S3cure!Pass'
# From a file
kubectl create secret generic tls-keys \
--from-file=tls.crt --from-file=tls.key
In YAML, values must be base64-encoded — but stringData lets you write plain text and Kubernetes encodes it on apply:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: admin
password: S3cure!Pass
Critical warning: base64 is not encryption. Anyone with get permission on secrets can decode them with one command:
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
That's exactly why the RBAC story for secrets matters (see section 6).
Secret types
Beyond the default Opaque, Kubernetes ships special-purpose types:
kubernetes.io/tls— storestls.crtandtls.key; Ingress controllers consume it directly for HTTPS.kubernetes.io/dockerconfigjson— stores registry credentials; reference it in a pod'simagePullSecretsto pull from a private registry.kubernetes.io/basic-auth,kubernetes.io/ssh-auth— for HTTP basic auth and SSH keys.
# Create a TLS secret for your Ingress
kubectl create secret tls myapp-tls --cert=tls.crt --key=tls.key
# Use a private-registry secret in a pod
spec:
imagePullSecrets:
- name: registry-credentials
5. Production Best Practices
Now that the mechanics are clear, here's what separates a working cluster from a secure one:
- Mark configuration immutable when possible.
immutable: trueon ConfigMaps and Secrets prevents accidental edits and improves performance (kubelet skips watching unchanged objects). Only do this when you can recreate the object instead of editing it — common in GitOps pipelines. - Never commit secrets to Git. A secret in a manifest file in your repo is a secret in your history, forever. Use Sealed Secrets, External Secrets Operator, or a vault like HashiCorp Vault to store the ciphertext, not the plaintext.
- Enable encryption at rest for secrets. By default secrets sit in etcd as base64. Configure
--encryption-provider-configwithaescbcorkmsso the data is actually encrypted on disk. - Restrict secret access with RBAC. Don't grant blanket
geton all secrets — a read ondb-credentialsis a read on the production password. Scope roles to namespaces and specific resource names. - Name secrets and configmaps clearly.
app-configanddb-credentialsbeatconfig1andsecret-final-v2when an on-call engineer needs to find the right object at 3 AM.
6. Debugging Configuration Issues
When a pod doesn't behave as expected, these commands reveal what was actually injected:
# What does the ConfigMap actually contain?
kubectl get configmap app-config -o yaml
# What does the Secret decode to? (be careful with this one)
kubectl get secret db-credentials -o jsonpath='{.data}' | base64 -d
# What env vars does the running container see?
kubectl exec myapp-7d4f8b9c6c-xk9j2 -- env | grep DB_
# What files were mounted?
kubectl exec myapp-7d4f8b9c6c-xk9j2 -- ls -la /etc/config
# Why won't the pod start? (missing key, bad volume reference)
kubectl describe pod myapp-7d4f8b9c6c-xk9j2
The most common failure — CreateContainerConfigError — means the pod couldn't resolve a configMapKeyRef or secretKeyRef. The describe output names the missing key; check the spelling and confirm the object exists in the same namespace as the pod.
Summary
ConfigMaps and Secrets are the difference between an image that runs anywhere and a container with a password baked in. The pattern to remember:
- ConfigMap — non-sensitive config, injected via env or volume.
- Secret — sensitive data, base64-encoded, needs RBAC and at-rest encryption to be trustworthy.
- Volume mounts update live; env vars require a rollout restart.
- Keep secrets out of Git — use Sealed Secrets, External Secrets Operator, or Vault.
Start by moving every hardcoded value out of your images into a ConfigMap. Then move the credentials into Secrets. Then lock down RBAC and enable encryption at rest. Your images get smaller, your deployments get portable, and your security posture stops depending on "nobody forks the repo."