KubernetesBeginner

Kubernetes ConfigMaps and Secrets: A Practical Guide

How to externalize configuration and sensitive values from your container images using ConfigMaps and Secrets — and why Secrets alone aren't actually encryption.

DevFieldGuideJuly 15, 2026 (updated July 25, 2026)6 min read
Share:

Baking configuration and credentials directly into a container image means rebuilding the image every time a value changes, and it means secrets end up in your image layers. ConfigMaps and Secrets exist to separate configuration from the image itself.

ConfigMaps — for non-sensitive configuration

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  FEATURE_FLAG_NEW_UI: "true"

Secrets — for sensitive values

yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  DATABASE_PASSWORD: cG9zdGdyZXNfcGFzc3dvcmQ=  # base64-encoded
bash
# Easier than hand-encoding base64 yourself
kubectl create secret generic app-secrets \
  --from-literal=DATABASE_PASSWORD=postgres_password
AspectConfigMapSecret
PurposeNon-sensitive configurationSensitive values (passwords, tokens, keys)
StoragePlain textBase64-encoded — not encrypted by default
Safe to commit to git?Yes, generallyNo — even encoded, it's a reversible real credential

Using either as environment variables

yaml
spec:
  containers:
    - name: app
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secrets

envFrom injects every key from the ConfigMap/Secret as an environment variable in one shot — useful when you have many values and don't want to list each individually.

Or mounted as files

yaml
spec:
  containers:
    - name: app
      volumeMounts:
        - name: config-volume
          mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: app-config

Each key becomes a file in the mounted directory, with the value as its contents — the right choice for applications that expect configuration as files (like a .env file or an nginx config) rather than environment variables, or for large values environment variables aren't well-suited to.

The important caveat: Secrets aren't encrypted by default

kubectl get secret app-secrets -o yaml shows the value base64-encoded, not encrypted — base64 is an encoding, trivially reversible (echo "..." | base64 -d), not a security measure. Anyone with kubectl access to read Secret objects in that namespace can decode them instantly.

Real protection requires layering on top of the base Secret object:

  • Encryption at rest — configuring the cluster's API server with an encryption provider so Secrets are actually encrypted in etcd, not just base64-encoded.
  • RBAC — restricting who can read Secret objects via Kubernetes' role-based access control, so "base64 isn't encryption" matters less if far fewer people/services can read the object in the first place.
  • External secret managers — tools like AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager integrated via an operator (e.g., External Secrets Operator), so the actual sensitive value lives in a purpose-built secret store, and Kubernetes only holds a reference or a short-lived synced copy.

A reasonable default approach

For low-sensitivity config: ConfigMaps, without a second thought. For genuinely sensitive values (database credentials, API keys, tokens): native Secrets are fine for getting started, but as soon as multiple people have cluster access or the values are genuinely high-stakes, layer on RBAC restrictions and consider an external secret manager rather than treating Secrets as sufficient protection on their own.

Immutable ConfigMaps and Secrets for safer rollouts

Kubernetes supports marking a ConfigMap or Secret immutable: true, which prevents any further edits to that object — sounds counterproductive, but it solves a real problem: the kubelet doesn't need to keep watching an immutable object for changes, reducing API server load on large clusters, and it protects against an accidental edit silently changing behavior for every pod referencing it.

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v2
data:
  LOG_LEVEL: "info"
immutable: true

The practical pattern that goes with this: instead of editing a ConfigMap in place, create a new versioned one (app-config-v2, app-config-v3) and update the Deployment to reference the new name. Combined with a rolling update, this naturally forces a real pod restart to pick up the new values — solving the "editing a ConfigMap doesn't restart pods automatically" problem from the mistakes list below as a side effect of the versioning pattern itself, rather than needing a separate restart step.

Projecting multiple sources into one volume

A projected volume combines a ConfigMap, a Secret, and other sources into a single mounted directory, useful when an application expects all of its configuration and credentials as files in one place rather than scattered across separate mount points:

yaml
volumes:
  - name: combined-config
    projected:
      sources:
        - configMap:
            name: app-config
        - secret:
            name: app-secrets

This is a purely organizational tool — it doesn't change how ConfigMaps or Secrets are created or managed, just how they're presented to the container's filesystem, which matters for applications (like some off-the-shelf tools) that expect a single config directory rather than assembling values from multiple mount paths themselves.

Once config and secrets are wired up, the deployment mechanism that actually ships changes matters just as much — Argo CD and GitOps is the pattern most teams reach for to keep these manifests reconciled automatically, and a crash right after a bad ConfigMap change is exactly the kind of thing debugging CrashLoopBackOff walks through.

Generating from a file instead of literal values

For a whole config file (an nginx config, a .env file) rather than a handful of key-value pairs, generating the ConfigMap directly from the file avoids retyping its contents into YAML by hand:

bash
kubectl create configmap nginx-config --from-file=nginx.conf
kubectl create secret generic tls-cert --from-file=tls.crt --from-file=tls.key

This is the same underlying object as a hand-written YAML manifest — kubectl create ... --from-file just generates it from existing content instead of requiring the value to be typed inline, which matters most for larger config files where a copy-paste into a YAML string would be error-prone.

Common mistakes

Common mistakes
  • Treating base64 encoding as if it were encryption, and concluding Secrets alone are "secure enough" without RBAC or encryption at rest layered on top.
  • Committing a Secret manifest with real values (even base64-encoded) into version control — it's permanently in the git history from that point forward, recoverable by anyone with repo access, decode included.
  • Putting genuinely large configuration files (not just a few key-value pairs) into environment variables via envFrom instead of mounting them as files — some applications and tools expect a real config file on disk, and very large environment variables can hit practical size limits.
  • Forgetting that updating a ConfigMap/Secret doesn't automatically restart pods using it as an environment variable — pods need to be restarted (or use a mounted-file approach with a reload mechanism) to actually pick up the new values, since envFrom values are set once at container start.
Advertisement

Frequently Asked Questions

Advertisement
DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in Kubernetes

View all