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

graph TD cm["ConfigMap updated<br/>(kubectl apply -f new-config.yaml)"] cm -->|"env var Pods"| envnote["No change — env vars are\nset once at container start,\nnever re-read afterward.\nRequires a Pod restart."] cm -->|"volume-mounted Pods"| volnote["File on disk updates within\n~60s (kubelet sync period) —\nbut the running process must\nitself notice and re-read it."]

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-encodedecho -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.

graph LR A["Secret in etcd:<br/>cGFzc3dvcmQxMjM="] -->|"base64 -d<br/>(no key needed)"| B["password123<br/>(plaintext)"]

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.