ConfigMaps and Secrets
Separating configuration from images. ConfigMaps vs Secrets, env vars vs volume mounts, which updates live, and what Secrets really protect.
The same container image should run in dev, staging and production. What changes between them is configuration, database hosts, feature flags, credentials, and Kubernetes provides two objects to supply it at runtime: ConfigMaps for ordinary values, Secrets for sensitive ones.
Getting this split right is what makes an image promotable rather than environment-specific.
Why configuration does not belong in the image
An image with DATABASE_URL baked in is a staging image or a production image, never both. Promoting a build then means rebuilding it, which means the artifact you tested is not the artifact you shipped, and rotating a credential means a rebuild and redeploy of everything that uses it.
Externalising configuration is the third factor of the twelve-factor methodology, and it is the difference between promoting a tested artifact and rebuilding a hopefully-identical one.
ConfigMap or Secret?
| ConfigMap | Secret | |
|---|---|---|
| For | Non-sensitive configuration | Passwords, tokens, certificates, keys |
| Stored as | Plain text in etcd | Base64 in the API, optionally encrypted at rest |
Shown by kubectl get -o yaml | Values in the clear | Base64, trivially decoded |
| Size limit | 1 MiB | 1 MiB |
| Separate RBAC | Yes | Yes (and it is the control that matters) |
The honest summary: a Secret is a ConfigMap with different defaults and its own RBAC surface. Base64 is encoding, not encryption, and anyone who can get secrets in a namespace can read every one of them.
What a Secret genuinely gives you is worth listing, because it is not nothing:
- A distinct RBAC resource, so you can grant ConfigMap access without granting Secret access.
- Values kept out of
kubectl describeoutput and most log lines. - Optional encryption at rest in etcd, which must be configured, on managed clusters this usually means enabling envelope encryption with a cloud KMS key.
tmpfsfor mounted Secret volumes, so the value never touches node disk.
What it does not give you is meaningful protection against someone with read access to the namespace. That is what external secret managers are for.
Two ways to consume them, and one important difference
Both objects can be injected as environment variables or mounted as files. The choice is not cosmetic.
As environment variables:
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: DB_PASSWORD
As mounted files:
volumes:
- name: config
configMap:
name: app-config
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/app
The difference that matters operationally:
Mounted volumes update live. Environment variables never do.
Change a ConfigMap consumed as a volume and the kubelet refreshes the file inside the running container within a minute or so. Change one consumed as an environment variable and the running pod keeps the old value until it is replaced, because environment variables are fixed at process start.
This catches people constantly: they update a ConfigMap, see no change, and conclude Kubernetes is broken. To pick up a changed environment variable you must restart the pods:
kubectl rollout restart deployment/payments-api
There is one exception to the live-update rule. A volume mounted with subPath does not update. It is mounted once and stays frozen. subPath is how you place a single config file into a directory that already has other contents, so this trips up exactly the people trying to be tidy.
Immutable ConfigMaps and Secrets
Setting immutable: true prevents any further change to the data:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v3
immutable: true
data:
LOG_LEVEL: info
Two benefits. It removes a whole class of accident, nobody edits production config in place, and it measurably reduces API server load, because the kubelet stops watching immutable objects for changes.
The pattern is to version the name (app-config-v3) and update the Deployment to reference the new one, which also makes config changes visible in your rollout history and rollback-able like any other change.
Where real secret management lives
For production, most teams keep the source of truth outside the cluster and sync it in:
- External Secrets Operator: reads from AWS Secrets Manager, Parameter Store, Vault or Azure Key Vault and creates native Kubernetes Secrets. The most common choice.
- Secrets Store CSI Driver: mounts values directly from an external store as files, optionally syncing to a Secret.
- Sealed Secrets: encrypts a Secret so the encrypted form is safe to commit to Git, decrypted only by a controller in the cluster. The usual answer for GitOps.
- Vault Agent Injector: injects secrets via a sidecar, with support for dynamic, short-lived credentials.
The common thread is that no plaintext secret is ever committed to a repository, and rotation happens in one place rather than across every manifest.
One thing to do regardless: never commit a Secret manifest with real base64 data to Git. Base64 is not obfuscation, and Git never forgets.
Explore further in this section
| Sub-topic | What you will learn |
|---|---|
| ConfigMaps | Create ConfigMaps and inject config as env vars or files |
| Secrets | Store sensitive values, registry credentials and TLS material |
Quick reference
| Task | Command |
|---|---|
| Create a ConfigMap | kubectl create configmap <n> --from-literal=K=V |
| From a file | kubectl create configmap <n> --from-file=app.conf |
| Create a Secret | kubectl create secret generic <n> --from-literal=K=V |
| Read a Secret value | kubectl get secret <n> -o jsonpath='{.data.K}' | base64 -d |
| Registry credentials | kubectl create secret docker-registry regcred --docker-server=... --docker-username=... --docker-password=... |
| TLS Secret | kubectl create secret tls <n> --cert=tls.crt --key=tls.key |
| Pick up an env var change | kubectl rollout restart deployment/<n> |
Key takeaways
- Externalise configuration so one image runs everywhere: that is what makes a tested artifact promotable.
- A Secret is not encrypted by default. Base64 is encoding; the real controls are RBAC and etcd encryption at rest.
- Mounted volumes update live; environment variables do not. Restart the pods after changing an env-var-backed value.
subPathmounts never auto-update, which surprises people trying to place a single file neatly.- Prefer
immutable: truewith versioned names: safer, and it lightens the API server. - Never commit real Secret manifests to Git. Use Sealed Secrets or the External Secrets Operator.
Check your understanding
A config change that did nothing
You update a ConfigMap consumed by pods as environment variables via envFrom. Minutes later the running pods still report the old value. What is happening?
What a Secret actually protects
A team moves database passwords from a ConfigMap into a Secret. What security improvement have they actually gained?
Frequently Asked Questions
What is the difference between a ConfigMap and a Secret?
Functionally they are near-identical key-value objects, both capped at 1 MiB and both consumable as environment variables or mounted files. The differences are that Secrets are a separate RBAC resource so access can be granted independently, their values are base64-encoded and omitted from kubectl describe, they can be encrypted at rest in etcd if the cluster is configured for it, and mounted Secret volumes are backed by tmpfs rather than node disk. Use ConfigMaps for ordinary settings and Secrets for anything sensitive, but do not mistake a Secret for encryption.
Why doesn't my pod pick up an updated ConfigMap?
Because it consumes the value as an environment variable. A process gets its environment once at start-up and it cannot be changed afterwards, so the pod keeps the old value until it is replaced. ConfigMaps mounted as volumes do update in place, typically within a minute. To apply an environment variable change, run kubectl rollout restart deployment/<name>, which triggers a normal rolling update with no downtime. Note that volumes mounted with subPath also never update, which is a separate and easily missed cause of the same symptom.
Are Kubernetes Secrets encrypted?
Not by default in the sense people usually mean. Values are base64-encoded in the API, which is trivially reversible. Kubernetes can encrypt Secrets at rest in etcd, but that must be configured explicitly, on managed clusters it generally means enabling envelope encryption with a cloud KMS key. Even then, anyone with get secrets permission in the namespace reads the plaintext through the API. RBAC is the real control, and for high-value credentials an external manager such as Vault or AWS Secrets Manager, synced in via the External Secrets Operator, is the right answer.
How do I store secrets in Git safely?
Never commit a Secret manifest containing real base64 data. It is not obfuscated and Git history is permanent. The two established approaches are Sealed Secrets, which encrypts a Secret with a cluster-held key so the encrypted file is safe to commit and only the in-cluster controller can decrypt it, and the External Secrets Operator, where Git holds only a reference to a secret stored in AWS Secrets Manager, Parameter Store or Vault. Both keep plaintext out of the repository while staying compatible with GitOps.
Is there a size limit on ConfigMaps?
Yes, 1 MiB, imposed by etcd. It is more than enough for configuration and too little for data. If you are approaching it, the content probably belongs somewhere else: a file fetched at start-up from object storage, a volume, or an init container that materialises it. Very large ConfigMaps also put real pressure on the API server, since every kubelet consuming one watches it for changes unless it is marked immutable.
Should I use environment variables or mounted files?
Mounted files when the value may change while the pod runs, when the application expects a config file on disk, or when the value is large or structured. Environment variables when the value is small, static for the pod's lifetime, and the application reads its configuration from the environment. A practical default is environment variables for simple scalars and volumes for anything file-shaped, while remembering that only volumes refresh without a restart.