Kubernetes ConfigMaps and Secrets¶
The problem: config baked into an image can't change without a rebuild¶
A container image with a database URL hardcoded into it (or worse, into application code) means "point this at a different database" requires rebuilding and redeploying the image — the same coupling problem Docker's "never bake secrets into a layer" guidance addresses at the image level. Kubernetes externalizes both non-sensitive configuration (ConfigMap) and sensitive values (Secret) into their own objects, injected into a Pod at runtime — the same image can run against dev, staging, and prod with entirely different config, with nothing about the image itself changing.
ConfigMap: externalized, non-sensitive configuration¶
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
FEATURE_FLAG_NEW_UI: "true"
Two distinct ways to consume this in a Pod:
# Option A: as environment variables
containers:
- name: api
envFrom:
- configMapRef:
name: app-config
# Option B: as files mounted into the filesystem
containers:
- name: api
volumeMounts:
- name: config
mountPath: /etc/app-config
volumes:
- name: config
configMap:
name: app-config
Option B mounts /etc/app-config/LOG_LEVEL and
/etc/app-config/FEATURE_FLAG_NEW_UI as individual files, each
containing the corresponding value — a shape some applications (ones
that expect config files, not env vars) need directly.
The update-propagation difference that actually matters operationally¶
Environment variables are frozen at container start. Updating the
ConfigMap does nothing to a container that already consumed it via
envFrom — the new value only takes effect on the next Pod restart
(a rolling restart, or the next deploy). This surprises people who
expect editing a ConfigMap to "just work" immediately.
Volume-mounted files update automatically on disk — kubelet periodically re-syncs mounted ConfigMap/Secret volumes (roughly every minute, though the exact interval is an implementation detail, not a guarantee to build tight logic around) without restarting the Pod. But this only changes what's on disk; the running application process still has to notice the file changed and re-read it — most applications don't watch their config files for changes and will keep running with the value they read at startup regardless. Getting genuine live-reload requires the application to explicitly support it (a file-watcher, or a periodic re-read), not just mounting the file and hoping.
Secret: same shape, but for sensitive values — with a critical caveat¶
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
password: cGFzc3dvcmQxMjM=
Consumed identically to a ConfigMap (envFrom: secretRef: or a
secret: volume) — same env-var-vs-file update-propagation behavior
applies identically.
The caveat that matters most: data.password above is not
encrypted. It's base64-encoded — echo -n "password123" | base64
produces exactly that string, and echo cGFzc3dvcmQxMjM= | base64 -d
reverses it instantly. Base64 is an encoding, not encryption; it
exists so arbitrary binary data survives being embedded in YAML/JSON
text, not to hide the value from anyone. Anyone with kubectl get
secret db-credentials -o yaml access can trivially decode it — the
actual protection is entirely in who is allowed to read Secret
objects at all, via RBAC (covered in a follow-up page),
not in the Secret object's own storage format.
Additionally, by default, etcd itself stores Secret data
unencrypted at rest — anyone with direct access to the etcd data
files (not even going through the Kubernetes API) can read every
Secret's base64 value directly. Production clusters should enable
encryption at rest for etcd
specifically to close this gap — it's a cluster-level configuration, not
something a Secret's own YAML can opt into.
Immutable ConfigMaps and Secrets¶
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
immutable: true
data:
LOG_LEVEL: "info"
immutable: true prevents any further edits to this object — attempting
to update it fails outright. Combined with versioning the name itself
(app-config-v2, then app-config-v3 for the next change, rather than
editing app-config in place), this makes config changes an explicit,
auditable new object plus a Deployment update to reference it, instead
of a silent in-place edit that could be forgotten, or that causes the
env-var-vs-volume-mount propagation confusion above. It also lets
kubelet skip watching this specific object for changes, a small but real
efficiency gain on very large clusters with many ConfigMaps.
Common pitfall¶
Treating a Kubernetes Secret as sufficient protection for genuinely
sensitive production credentials (a payment processor API key, a root
database password) — and, worse, committing a Secret's YAML manifest to
git because "it's a Secret, it must be safe" — is the most common
security misunderstanding here. The base64 value is fully recoverable by
anyone who can read the file, no different from committing the
plaintext password directly. Real secrets belong in a dedicated secrets
manager (HashiCorp Vault, a cloud provider's KMS-backed secret store),
synced into the cluster via something like the External Secrets
Operator, with the native Kubernetes Secret object treated as a
runtime delivery mechanism to the Pod, not as the system of record or a
safe place to hand-author sensitive values directly.