Ch. 14 · Kubernetes

Kubernetes ConfigMaps Versus Secrets

ConfigMap or Secret? Why base64 isn't encryption, what actually protects a Secret, and how env vars and mounted files behave on update.

~6 min readbeginnerupdated Oct 7, 2026

Both objects do the same mechanical job: they hold key-value data outside the container image so the same image can run in dev, staging and production with different settings. The difference is what Kubernetes and your cluster administrators do around them. A ConfigMap is ordinary configuration. A Secret is configuration that the cluster treats with extra care, but only if the cluster has been set up to do so.

Most interview questions on this topic are really one question in disguise: “Is a Secret secure?” The right answer is “it depends on three things outside the Secret object”, and the rest of this guide explains those three things. Examples target Kubernetes 1.30 and later.

Same shape, different handling

Here is the configuration for an orders API split the way most teams split it:

apiVersion: v1
kind: ConfigMap
metadata:
  name: orders-config
  namespace: shop
data:
  LOG_LEVEL: "info"
  PAYMENT_TIMEOUT_MS: "2500"
  FEATURE_SPLIT_SHIPMENTS: "true"
---
apiVersion: v1
kind: Secret
metadata:
  name: orders-db
  namespace: shop
type: Opaque
stringData:
  username: orders
  password: "pg-Hunter2!"
yaml

stringData is a write-only convenience: the API server base64-encodes it into data when it stores the object. The log level and timeout are in a ConfigMap because leaking them hurts nobody. The database password is in a Secret because leaking it does.

ConfigMaps and Secrets share a 1 MiB size limit, and both are namespaced, so a Pod can only reference ones in its own namespace. Secrets also have types. Opaque is the generic one; kubernetes.io/tls expects tls.crt and tls.key, and kubernetes.io/dockerconfigjson holds registry credentials used by imagePullSecrets. The type lets the API server validate the keys.

Base64 is a format, not a lock

This is the misconception interviewers check for first. Generate the Secret above with kubectl and look at it (this used --dry-run=client, which builds the object locally without contacting a cluster):

kubectl create secret generic orders-db -n shop \
  --from-literal=username=orders \
  --from-literal=password='pg-Hunter2!' \
  --dry-run=client -o yaml
Terminal
apiVersion: v1
data:
  password: cGctSHVudGVyMiE=
  username: b3JkZXJz
kind: Secret
metadata:
  name: orders-db
  namespace: shop
yaml

Decoding takes one command and no key:

$ echo cGctSHVudGVyMiE= | base64 -d
pg-Hunter2!
Terminal

Base64 exists so binary values (a keystore, a TLS key) can travel inside JSON and YAML. It hides nothing. So if you commit that YAML to Git, you’ve committed the password. Teams that want Secrets in Git use Sealed Secrets, SOPS, or the External Secrets Operator pulling from Vault or a cloud secret manager, so only an encrypted form or a reference is stored in the repository.

What actually protects a Secret

Three mechanisms, none of them visible in the Secret manifest.

RBAC. Who can get, list or watch Secrets in a namespace? list is worth singling out: a role that can list Secrets receives every value in the response, not just the names. A common audit finding is a CI service account with secrets: ["*"] across the cluster. Check with:

kubectl auth can-i get secrets -n shop --as=system:serviceaccount:shop:ci-deployer
Terminal

Also note that anyone who can create a Pod in a namespace can mount any Secret in that namespace into it and read it. Pod-creation rights are effectively Secret-read rights for that namespace. See RBAC and least privilege.

Encryption at rest. By default the API server stores Secrets in etcd unencrypted (the base64 form). Cluster admins enable encryption with an EncryptionConfiguration file passed to the API server, ideally with a KMS v2 provider so the key lives in a cloud KMS or HSM. Managed services such as EKS, GKE and AKS either do this by default or offer it as a setting. If someone gets an etcd backup from a cluster without it, they have every password.

Node scoping. The kubelet fetches a Secret only when a Pod scheduled on that node references it, and Secret volumes are stored in tmpfs, so they don’t land on the node’s disk. A compromised node exposes the Secrets of Pods running on it, not the whole cluster’s.

If an interviewer asks “Are Kubernetes Secrets secure?”, walk through those three, then mention that for high-value credentials many teams keep the source of truth in an external secret manager and use short-lived credentials where possible.

Environment variables or mounted files

There are two ways to get the values into a container, and they behave differently when the value changes.

apiVersion: v1
kind: Pod
metadata:
  name: orders
  namespace: shop
spec:
  containers:
    - name: api
      image: registry.example.com/orders:4.2.0
      envFrom:
        - configMapRef:
            name: orders-config
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: orders-db
              key: password
      volumeMounts:
        - name: db-creds
          mountPath: /etc/orders/db
          readOnly: true
  volumes:
    - name: db-creds
      secret:
        secretName: orders-db
        defaultMode: 0400
yaml

Environment variables are read once, at container start. If you rotate the password in the Secret, DB_PASSWORD in the running container still holds the old value until the container restarts. Env vars also leak more easily: they show up in crash dumps, /proc/<pid>/environ, and debug endpoints that print the environment.

Mounted volumes refresh. When the Secret changes, the kubelet updates the files in /etc/orders/db after a delay (its sync period plus the cache TTL, typically under a minute or two). Your application still has to re-read the file; most don’t, unless they watch for changes or open the file on every new connection. One exception catches people out: a volume mounted with subPath never receives updates.

If a referenced Secret or ConfigMap doesn’t exist, or the key is missing, the container won’t start and the Pod shows CreateContainerConfigError. That’s covered in the CreateContainerConfigError guide.

Interview tip

For config that must never change under a running Pod, set immutable: true on the ConfigMap or Secret. The kubelet stops watching it, which also reduces API server load in large clusters. To change it, create a new object with a new name (for example orders-config-v7) and roll the Deployment.

Rolling out a changed value

Since env vars don’t refresh and most apps don’t re-read files, the dependable way to apply a new value is a rollout. Two common patterns:

  1. Put a hash of the config in a Pod template annotation (Helm’s checksum/config pattern, or Kustomize’s configMapGenerator, which appends a content hash to the name). Changing the config changes the template, so the Deployment rolls.
  2. Run kubectl rollout restart deployment/orders -n shop after updating the object, which stamps a restart annotation onto the template.

The second is fine by hand but easy to forget; the first ties the rollout to the change itself. More detail is in how ConfigMap updates reach Pods.

Interview practice

“A developer stores the Stripe API key in a ConfigMap, arguing that Secrets are only base64 anyway, so there’s no difference. Is the developer right?”

Answer

They’re right that base64 isn’t protection, but wrong about there being no difference. The difference isn’t in the encoding; it’s in how the cluster treats the two kinds of object.

Clusters commonly grant broad read access to ConfigMaps, since they hold non-sensitive settings: developers, dashboards and monitoring agents often have get and list on them. Secrets are where RBAC restrictions are applied, where encryption at rest is configured (an EncryptionConfiguration usually lists secrets as a resource, not configmaps), and where audit policies log access. Many tools also redact Secret values in output and avoid logging them. Putting the Stripe key in a ConfigMap opts it out of all of those controls.

I’d move it to a Secret, check that only the payment service’s service account and a small ops group can read Secrets in that namespace, confirm encryption at rest is on, and consider sourcing it from an external secret manager so rotation doesn’t depend on someone editing YAML.

Where to go next

More in Kubernetes

read ✓Kubernetes · mid

Kubernetes RBAC and Least-Privilege Access

Build a least-privilege Role for a CI deployer, verify it with kubectl auth can-i, and spot grants like pods/exec that quietly escalate.

~6 min readread →
esc