All articles
Tutorials

Kubernetes ConfigMap vs Secret Explained

Learn when to use a Kubernetes ConfigMap vs Secret, why base64 is not encryption, and how to inject each into pods safely with env vars or volumes.

Kubernetes ConfigMap vs Secret Explained cover
10 min read

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 data values are base64-encoded, not encrypted. Anyone who can kubectl get secret -o yaml can 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 stringData when 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:

MistakeWhat goes wrong
Password or API key in a ConfigMapClear-text values in etcd and in any ConfigMap get/list
Assuming Secret base64 means encryptionecho VALUE | base64 -d recovers the plaintext instantly
Committing Secret YAML with data to GitThe repo becomes a credential dump (base64 is reversible)
Expecting env var updates without a restartPods keep stale values until the container restarts
Broad get/list/watch on SecretsAny 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.

DimensionConfigMapSecret
Intended dataNon-sensitive configSensitive credentials and keys
ExamplesLOG_LEVEL, feature flags, upstream URLs, nginx.confDB passwords, API tokens, TLS certs/keys, dockerconfigjson
Primary fieldsdata (UTF-8), optional binaryData (base64)data (base64), optional stringData (plain, converted by API)
Built-in typesNoneOpaque, kubernetes.io/tls, kubernetes.io/dockerconfigjson, service-account tokens, and others
Default etcd storageUnencrypted unless API encryption is configuredSame: unencrypted unless encryption at rest is enabled
Node deliverySent when a Pod needs itSame, with kubelet keeping Secret volume data in tmpfs
RBAC resourceconfigmapssecrets (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-app

Create 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=true

The equivalent declarative manifest keeps the same keys under data in plain text:

app-config.yaml
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:

app-with-configmap.yaml
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.conf

Apply 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_LEVEL

You 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):

db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: demo-app
type: Opaque
stringData:
  DB_USERNAME: app
  DB_PASSWORD: change-me-now

Or 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:

app-with-secret.yaml
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-secret

Confirm 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_USERNAME

Typed 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-now

Official 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:

  1. Enable encryption at rest for Secret resources on the API server (managed clouds often expose this as a cluster setting).
  2. Split RBAC so app ServiceAccounts can get only the Secrets they mount. Avoid namespace-wide list/watch on Secrets for workloads.
  3. 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.
  4. Never commit live Secret manifests with real data or stringData to Git. Use sealed-secrets, SOPS, External Secrets Operator, or a CI-injected create step.
  5. 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:

  1. 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.
  2. Is it ordinary runtime config? Log levels, non-secret URLs, feature flags, and file-based app config belong in a ConfigMap.
  3. 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.
  4. Will many Pods share the same non-secret settings? One ConfigMap referenced by several Deployments beats copy-pasted env blocks.
  5. 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:

KeyObject
APP_ENV, LOG_LEVEL, FEATURE_*ConfigMap
Upstream service DNS names (no credentials in the URL)ConfigMap
DB_PASSWORD, API_TOKEN, HMAC keysSecret
TLS cert/key for IngressSecret (kubernetes.io/tls)
Private registry pull authSecret (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.

  1. 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.

  2. 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.

  3. 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."

  4. 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.

Frequently asked questions

Share𝕏

Writer

  • Ilyas Rufai

    Technical content writer and DevSecOps specialist focused on cloud-native security and developer experience

Need help with your technical content?

We help B2B SaaS teams turn complex products into clear documentation and content that developers actually use.

Book a call
Kubernetes ConfigMap vs Secret Explained | Reclear