TL;DR
- Use a ConfigMap for non-sensitive settings (feature flags, URLs, log levels, config files). Use a Secret for credentials, tokens, TLS material, and registry auth.
- Secret
datavalues are base64-encoded, not encrypted. Anyone who cankubectl get secret -o yamlcan decode them. Encryption at rest is optional and off by default in many clusters. - Both inject the same ways: individual env keys,
envFrom, or volume mounts. Env vars freeze at container start; mounted files can refresh, but apps often still need a restart. - Prefer
stringDatawhen authoring Secrets in YAML, restrict Secret RBAC tightly, and move high-risk credentials to an external secret manager when etcd alone is not enough.
You ship a Deployment with DB_PASSWORD in a ConfigMap because "it is just config," then discover the value in every kubectl get configmap -o yaml, every CI log that dumps manifests, and every RBAC role that grants broad ConfigMap read. Or you put the password in a Secret, see base64 in the YAML, and assume the cluster encrypted it. It did not.
In this article, you will get a clear ConfigMap vs Secret decision rule, working create-and-inject examples, and the security caveats that most "explained" posts bury: base64 is not encryption, env vars do not hot-reload, and Kubernetes Secrets are only as safe as your RBAC and etcd settings.
Note
What you need: kubectl 1.28+, access to a cluster (kind, minikube, EKS, GKE, or AKS), and permission to create ConfigMaps, Secrets, and Deployments in a non-production namespace. The examples use namespace demo-app.
Why teams mix up ConfigMaps and Secrets
Both objects store key/value data outside the container image and inject it into Pods. The API shapes look almost identical, so teams treat them as interchangeable storage with different names.
That is the failure mode. Kubernetes documents ConfigMaps for non-confidential configuration and Secrets for confidential data. The platform also applies extra controls around Secrets (separate RBAC resource, tmpfs mounts on nodes, optional encryption providers). If you put a database password in a ConfigMap, you skip those controls on purpose.
Common mistakes look like this:
| Mistake | What goes wrong |
|---|---|
| Password or API key in a ConfigMap | Clear-text values in etcd and in any ConfigMap get/list |
| Assuming Secret base64 means encryption | echo VALUE | base64 -d recovers the plaintext instantly |
Committing Secret YAML with data to Git | The repo becomes a credential dump (base64 is reversible) |
| Expecting env var updates without a restart | Pods keep stale values until the container restarts |
Broad get/list/watch on Secrets | Any subject with those verbs can read every Secret in the namespace |
ConfigMap vs Secret side by side
Both are namespaced API objects. Both support data maps. Both can feed env vars or files. The differences that matter in production are intent, encoding, typing, and how the control plane treats the object.
| Dimension | ConfigMap | Secret |
|---|---|---|
| Intended data | Non-sensitive config | Sensitive credentials and keys |
| Examples | LOG_LEVEL, feature flags, upstream URLs, nginx.conf | DB passwords, API tokens, TLS certs/keys, dockerconfigjson |
| Primary fields | data (UTF-8), optional binaryData (base64) | data (base64), optional stringData (plain, converted by API) |
| Built-in types | None | Opaque, kubernetes.io/tls, kubernetes.io/dockerconfigjson, service-account tokens, and others |
| Default etcd storage | Unencrypted unless API encryption is configured | Same: unencrypted unless encryption at rest is enabled |
| Node delivery | Sent when a Pod needs it | Same, with kubelet keeping Secret volume data in tmpfs |
| RBAC resource | configmaps | secrets (grant separately and more tightly) |
Use the table as a filter, not a suggestion that Secrets are "secure by default." They are the right kind of object for sensitive values. Hardening still depends on encryption at rest, least-privilege RBAC, and what you put in Git.
How to create and inject a ConfigMap
Start with non-sensitive settings so the Pod can change environment without rebuilding the image. Create the namespace once if it does not exist:
kubectl create namespace demo-appCreate a ConfigMap from literals (useful for a few keys) or from a file (useful for full config documents):
kubectl create configmap app-config \
--namespace demo-app \
--from-literal=APP_ENV=staging \
--from-literal=LOG_LEVEL=info \
--from-literal=FEATURE_CHECKOUT=trueThe equivalent declarative manifest keeps the same keys under data in plain text:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo-app
data:
APP_ENV: staging
LOG_LEVEL: info
FEATURE_CHECKOUT: "true"
nginx.conf: |
worker_processes 1;
events { worker_connections 1024; }Inject individual keys as environment variables, or mount the whole object as files. The Deployment below does both so you can see the two patterns in one place:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command: ["sh", "-c", "env | grep -E 'APP_ENV|LOG_LEVEL'; sleep 3600"]
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
volumes:
- name: nginx-config
configMap:
name: app-config
items:
- key: nginx.conf
path: nginx.confApply and verify the env keys appear inside the container:
kubectl apply -f app-config.yaml
kubectl apply -f app-with-configmap.yaml
kubectl exec -n demo-app deploy/api -- printenv APP_ENV LOG_LEVELYou should see staging and info. For bulk injection of every key, use envFrom with configMapRef instead of listing each configMapKeyRef.
How to create and inject a Secret
Use a Secret for anything that would hurt you if it leaked in a ConfigMap dump or a CI artifact. Prefer stringData in manifests so you do not hand-encode values (the API server converts them to base64 data on write):
apiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: demo-app
type: Opaque
stringData:
DB_USERNAME: app
DB_PASSWORD: change-me-nowOr create it imperatively without writing the password into a tracked file:
kubectl create secret generic db-secret \
--namespace demo-app \
--from-literal=DB_USERNAME=app \
--from-literal=DB_PASSWORD='change-me-now'Wire the Secret into the Pod the same way you wire a ConfigMap, but with secretKeyRef / secretRef / secret volume sources:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-secret
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSWORD
volumeMounts:
- name: db-creds
mountPath: /var/run/secrets/db
readOnly: true
volumes:
- name: db-creds
secret:
secretName: db-secretConfirm the Secret exists and that the Pod can read the keys (do this only in a sandbox; avoid printing production secrets into shared terminals or CI logs):
kubectl apply -f db-secret.yaml
kubectl apply -f app-with-secret.yaml
kubectl get secret db-secret -n demo-app -o jsonpath='{.data.DB_USERNAME}' | base64 -d; echo
kubectl exec -n demo-app deploy/api -- printenv DB_USERNAMETyped Secrets matter for platform features. A kubernetes.io/dockerconfigjson Secret is what you attach via imagePullSecrets when private registry pulls fail with unauthorized. That path is covered in detail in How to Fix the ImagePullBackOff Error in Kubernetes. TLS Secrets (kubernetes.io/tls) feed Ingress and other controllers that expect tls.crt and tls.key.
Tip
Mark ConfigMaps and Secrets as immutable: true when values should not change in place. Immutability blocks accidental updates and can reduce kubelet watches for workloads that pin a specific config generation.
Why base64 is not encryption
This is the gap most ConfigMap vs Secret explainers underplay. When you kubectl get secret db-secret -o yaml, values under data look obfuscated. They are not.
Base64 is a transport encoding so binary-safe payloads can sit in JSON/YAML. Decoding requires no key:
echo 'Y2hhbmdlLW1lLW5vdw==' | base64 -d
# change-me-nowOfficial Kubernetes docs state that Secrets are stored unencrypted in etcd by default, and that anyone with API or etcd access can retrieve them. Encoding does not change that.
Treat these as the minimum hardening steps when you rely on in-cluster Secrets:
- Enable encryption at rest for Secret resources on the API server (managed clouds often expose this as a cluster setting).
- Split RBAC so app ServiceAccounts can
getonly the Secrets they mount. Avoid namespace-widelist/watchon Secrets for workloads. - Prefer volume mounts over env vars when you can. Env vars are easy to leak via
printenv, crash dumps, and child processes; files on a tmpfs mount are still sensitive but easier to scope per container. - Never commit live Secret manifests with real
dataorstringDatato Git. Use sealed-secrets, SOPS, External Secrets Operator, or a CI-injected create step. - Rotate credentials after any suspected exposure. Changing the Secret object alone is not enough until Pods restart (or remount and reload).
When to use ConfigMap vs Secret
Use this decision order when you add a new key to a workload:
- Would disclosure of this value create a security or compliance incident? If yes, it belongs in a Secret (or an external secret store), never a ConfigMap.
- Is it ordinary runtime config? Log levels, non-secret URLs, feature flags, and file-based app config belong in a ConfigMap.
- Does a Kubernetes controller expect a typed Secret? Registry pull credentials, TLS pairs, and bootstrap tokens use Secret types. Do not reinvent them as ConfigMaps.
- Will many Pods share the same non-secret settings? One ConfigMap referenced by several Deployments beats copy-pasted env blocks.
- Do you need rotation, audit, or cross-cluster sharing? Stay on Kubernetes Secrets only for simple cases. Escalate to Vault or cloud secret managers when those requirements appear.
A practical split for a typical API Deployment:
| Key | Object |
|---|---|
APP_ENV, LOG_LEVEL, FEATURE_* | ConfigMap |
| Upstream service DNS names (no credentials in the URL) | ConfigMap |
DB_PASSWORD, API_TOKEN, HMAC keys | Secret |
| TLS cert/key for Ingress | Secret (kubernetes.io/tls) |
| Private registry pull auth | Secret (kubernetes.io/dockerconfigjson) |
If you are still choosing tools for how config reaches the cluster (Helm values, GitOps, Ansible), keep the same rule at the source: non-secret values may live in Git; secret values need encryption or an external store before they ever become a Kubernetes object. That boundary pairs cleanly with the provisioning vs configuration split in Terraform vs Ansible: When to Use Which.
When this approach breaks down
In-cluster ConfigMaps and Secrets are the right default for many apps. They stop being enough in a few common cases.
-
Env var injection and live updates. Changing a ConfigMap or Secret does not update environment variables in running containers. Mounted files can update later (kubelet sync period), but many processes read config once at startup. Plan on
kubectl rollout restart(or a reload signal your app understands) after every sensitive rotation. -
etcd and API access as the trust boundary. If an attacker can read etcd or impersonate a principal with Secret
get/list, every in-cluster Secret in scope is exposed. Encryption at rest helps against etcd disk theft; it does not help against a compromised API credential with Secret permissions. -
GitOps with raw Secret manifests. Declarative delivery fights with confidential data. Teams either encrypt manifests (SOPS, Sealed Secrets) or stop storing values in Git and sync from an external manager. Without one of those patterns, "Secrets in Kubernetes" becomes "Secrets in the repo."
-
Multi-cluster and compliance-heavy estates. Per-cluster Opaque Secrets do not give you centralized rotation policies, break-glass audit, or a single inventory of what exists. At that point the Kubernetes Secret is a delivery cache, not the system of record.

