open atlas
↑ Back to track
Deployment & Infra DEP · 10 · 02

ConfigMaps and secrets

Inject config and secrets at runtime, never bake them into the image. ConfigMaps and Secrets feed env vars or mounted files — but base64 is encoding, not encryption, so enable encryption-at-rest and RBAC.

DEP Middle ◷ 16 min
Level
FoundationsJuniorMiddleSenior

A staging build gets promoted to production and immediately starts writing to the staging database. The image was built with DATABASE_URL=postgres://staging... baked into an ENV line, so “promote the image” silently shipped staging config with it. Worse, git log shows the previous release embedded an API token the same way — it is now in every registry, every developer’s docker pull, and the layer history forever. Both bugs have one root cause: configuration that belongs to the environment was frozen into the artifact.

One image, many environments: the 12-factor split

Ask yourself: if you build the image in CI and then promote it to production, what changes between environments? Everything that changes belongs outside the image — not frozen inside it.

A container image is meant to be built once and run anywhere — dev, staging, production — identically. The moment you bake an environment-specific value into it (a database URL, a feature flag, an API key), you break that promise: the same image can no longer run in two environments, and anything secret you froze in is now permanent in the layer history. The 12-factor rule is to keep config in the environment, not the code, and inject it at runtime.

Kubernetes gives you two objects for this. A ConfigMap holds non-confidential key/value config — “an API object used to store non-confidential data in key-value pairs.” A Secret holds the same shapes for sensitive data. Both are decoupled from the Pod: you can change the config without rebuilding the image, and you can run the identical image in staging and prod by pointing it at different ConfigMaps and Secrets.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  FEATURE_NEW_CHECKOUT: "true"
  app.properties: |
    cache.ttl=300
    cache.size=2000
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  # base64 of "postgres://app:s3cr3t@db:5432/app" — encoding, NOT encryption
  DATABASE_URL: cG9zdGdyZXM6Ly9hcHA6czNjcjN0QGRiOjU0MzIvYXBw

Two ways in: env vars vs. mounted files

A Pod consumes a ConfigMap or Secret in one of two shapes, and the choice has a runtime consequence most people learn the hard way. You can project values as environment variables (envFrom for a whole map, or valueFrom.configMapKeyRef / secretKeyRef for one key), or mount them as files in a volume, where each key becomes a file. Env vars are convenient for flat scalars; volumes are the right call for whole config files (a TLS cert, an application.yaml) and for anything you want to update without redeploying.

The consequence: mounted files update live; env vars do not. When a ConfigMap consumed in a volume is updated, “projected keys are eventually updated as well” — the kubelet refreshes them on its periodic sync. But “ConfigMaps consumed as environment variables are not updated automatically and require a pod restart.” The same split holds for Secrets. So if you change a value that the app reads from an env var, nothing happens until the Pod restarts; change a value mounted as a file, and the file updates in place — though your app still has to re-read it.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 3
  selector:
    matchLabels: { app: app }
  template:
    metadata:
      labels: { app: app }
    spec:
      containers:
        - name: app
          image: registry.example.com/app:1.4.2   # same image, every env
          envFrom:
            - configMapRef:
                name: app-config        # whole ConfigMap as env vars
          env:
            - name: DATABASE_URL        # one secret key as an env var
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: DATABASE_URL
          volumeMounts:
            - name: config-files
              mountPath: /etc/app        # app.properties shows up as a file; updates live
              readOnly: true
      volumes:
        - name: config-files
          configMap:
            name: app-config
Why this works

“Update live” is not the whole story. The kubelet only refreshes mounted volumes on its periodic sync, so the delay can be “as long as the kubelet sync period + cache propagation delay” — seconds to a minute, not instant. And the file changing on disk does nothing unless your application watches the file and reloads it; most apps read config once at startup. So mounting as a volume is necessary but not sufficient for live rotation — the app has to participate.

The senior caveat: base64 is not security

This is the failure that gets teams breached. In a Secret’s data field the values are base64-encoded, not encrypted — and base64 is trivially reversible (echo ... | base64 -d). By default “Kubernetes Secrets are, by default, stored unencrypted in the API server’s underlying data store (etcd).” So a Secret object is not, on its own, secret: “Anyone with API access can retrieve or modify a Secret, and so can anyone with access to etcd.” A Secret differs from a ConfigMap mainly in intent and tooling (it is typed, kept out of some logs, can be RBAC-scoped) — not in any cryptography.

To make Secrets actually safe, Kubernetes says to “take at least the following steps”: enable Encryption at Rest so the bytes in etcd are encrypted, and configure RBAC with least-privilege access so only the workloads and people who need a Secret can get it. Layer on restricting Secret access to specific containers, and for real secret management reach for external secret stores — HashiCorp Vault, the External Secrets Operator, or a cloud secret manager (AWS/GCP/Azure) — which keep the source of truth outside the cluster, support audit and rotation, and sync into Kubernetes on demand. The cardinal sin to avoid: treating a base64 Secret manifest as safe and committing it to git. base64 in a repo is plaintext to anyone who clones it.

PropertyConfigMapSecret
PurposeNon-confidential configSensitive data (tokens, passwords, keys)
On-the-wire encodingPlain UTF-8 in database64 in data (encoding, not encryption)
Encrypted at rest in etcd?NoNot by default — must enable encryption-at-rest
Who can read itAnyone with API/etcd accessAnyone with get secret RBAC — so scope it tightly
Consumed asenv vars or volume filesenv vars or volume files (same two shapes)

Immutability and rotation

When you change a config value in production, what should happen to the pods that are already running? The answer depends on which of the two levers you reach for — and each has a different failure mode when misapplied.

Two operational levers finish the picture. Mark a ConfigMap or Secret immutable (add a top-level immutable: true) when its value should never change in place. This buys two things: protection from accidental updates that would silently break running Pods, and a performance win — the kubelet stops watching immutable objects for changes, which materially reduces API-server load on large clusters. The tradeoff is that you cannot edit an immutable object; to change it you create a new one (often suffixed with a hash) and roll the Deployment to point at it.

That naturally leads to rotation. Because env-var values are frozen at Pod start, rotating a Secret consumed as an env var does nothing until you restart the Pods — and a deliberate kubectl rollout restart is the honest way to apply it. A Secret mounted as a file can rotate without a restart, but only if the application watches the file and reloads. Many teams therefore standardize on either “mount + app watches” for true zero-downtime rotation, or “immutable + hash-suffixed names + rollout” so a config change is a normal, auditable, rollback-able deploy.

Pick the best fit

A production app must rotate its database password with zero downtime. How should the credential be delivered to the Pods?

Quiz

A teammate says the Secret is safe because the values are base64-encoded. What is the accurate correction?

Quiz

You edit a ConfigMap value that the app reads from an environment variable. The running Pods don't pick up the change. Why?

Recall before you leave
  1. 01
    Why is base64 in a Kubernetes Secret not security, and what must you actually do to protect Secrets?
  2. 02
    When does changing a ConfigMap or Secret value actually take effect in a running Pod, and how does the consumption shape decide it?
Recap

Configuration that belongs to the environment must be injected at runtime, not baked into the image — bake it in and the same artifact can no longer move across environments, and any secret you froze lives in the layer history forever. Now when you see a secretKeyRef referencing a Secret, ask yourself: is that Secret encrypted at rest, tightly RBAC-scoped, and managed outside git? If not, you have a base64 string with a false sense of security. Kubernetes gives you a ConfigMap for non-confidential key/value config and a Secret for sensitive data, both decoupled from the Pod so one image runs everywhere by pointing at different objects. Each is consumed in one of two shapes — environment variables or files in a mounted volume — and the shape determines rotation: env vars are frozen at container start and need a Pod restart, while mounted files refresh on the kubelet sync but only take effect if the app re-reads them. The senior trap is that a Secret’s base64 is encoding, not encryption: Secrets sit unencrypted in etcd by default and anyone with get-secret RBAC or etcd access can decode them, so you must enable encryption-at-rest, scope access with least-privilege RBAC, never commit a Secret manifest to git, and reach for an external secret manager for anything that truly matters. Finally, mark config immutable to prevent accidental edits and cut kubelet watch load, accepting that rotation then means a new hash-named object plus a rollout.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.