Blog · May 13, 2025 · Updated on September 7, 2026 · 13 min read

ConfigMap and Secret in Kubernetes

A brown leather journal, closed with a leather strap and adorned with an antique key charm
Photo: RDNE Stock project / Pexels

A Kubernetes ConfigMap stores configuration as key-value pairs or whole files, separate from the container image. A Secret does the same for passwords, tokens and certificates. Both reach the Pod the same two ways: as an environment variable or as a file in a volume. The difference sits not in the structure but in how Kubernetes treats the data: a Secret is base64-encoded, distributed only to nodes that need it, and lockable with encryption at rest and RBAC. It is not encrypted by default.

That is where a lot of projects come undone: I keep seeing Secrets in plain text in Git because someone mistook base64 for encryption. It is an easy mistake to fix once you know the limits of these two objects.

I run Kubernetes clusters in production and wrote the Kubernetes Practical Guide for Rheinwerk Computing (2024); ConfigMaps and Secrets get their own section there, in chapter 3. Here is the short version with the workflow I run in my own clusters. You can find all my Kubernetes articles collected on the Kubernetes page.

Why configuration does not belong in the image

A container image holds everything your application needs to run: runtime, libraries, code. Two things still do not belong in it: configuration that differs by environment, and anything secret. Otherwise you build three images for development, test and production that differ only in a database address, with your password sitting in every image layer ever pushed to the registry.

Kubernetes separates that with two objects. The ConfigMap holds the configuration, the Secret holds the credentials. The same image then runs in every environment, only the ConfigMap and Secret next to it differ. That is also the foundation for much of what follows: a Deployment is only this easy to roll out because the image knows nothing about its environment. How the Deployment handles the Pod template is covered in Kubernetes Deployment: rollouts explained.

Kubernetes ConfigMap: structure and three ways into the Pod

A ConfigMap has two data fields: data for text and binaryData for base64-encoded binary data. Under data you store individual values, such as a log level, or a whole file as a multi-line string. All values are strings, so a port gets quotes. For the example I set up two ConfigMaps: one with environment variable values, one with a JSON file.

apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-env
data:
  LOG_LEVEL: "info"
  DB_HOST: "postgres.shop.svc"
  DB_PORT: "5432"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-files
data:
  config.json: |
    {
      "currency": "EUR",
      "featureFlags": ["checkout-v2"]
    }

This data reaches the Pod in three ways: as a single environment variable through env with configMapKeyRef, picking one specific key; as all keys of a ConfigMap at once through envFrom with configMapRef; or as a volume, where every key becomes a file in the mount directory and the value is the file content. The Pod manifest below shows envFrom and a volume together and already sets a password from the Secret I create in the next section.

apiVersion: v1
kind: Pod
metadata:
  name: shop
spec:
  containers:
    - name: shop
      image: nginx
      envFrom:
        - configMapRef:
            name: shop-env
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: shop-db
              key: DB_PASSWORD
      volumeMounts:
        - name: config
          mountPath: /etc/shop
          readOnly: true
  volumes:
    - name: config
      configMap:
        name: shop-files
        items:
          - key: config.json
            path: config.json

After kubectl apply, check with kubectl exec shop -- env whether LOG_LEVEL, DB_HOST and DB_PORT are set, and with kubectl exec shop -- cat /etc/shop/config.json whether the file arrived. items restricts which keys land in the volume; without it, all of them show up. Mark either reference optional: true and the Pod starts even if the ConfigMap is missing.

My rule of thumb: short values like log levels, hostnames or feature flags as environment variables, anything with structure like JSON, YAML or an nginx.conf as a file. Squeezing a JSON object into an environment variable works, but nobody wants to debug it. The fourth way, reading the ConfigMap through the Kubernetes API from inside the application, I rarely see: it couples the application to Kubernetes.

Kubernetes Secrets: base64 is not encryption

A Secret looks almost like a ConfigMap. Three things differ: it has a type field, values under data have to be base64-encoded, and for plain text there is stringData, which Kubernetes encodes itself. In manifests I always use stringData, so no mistake happens encoding by hand.

apiVersion: v1
kind: Secret
metadata:
  name: shop-db
type: Opaque
stringData:
  DB_USER: shop
  DB_PASSWORD: just-an-example

That brings us to the most important point of this article: base64 is an encoding, not encryption. Anyone allowed to read the Secret gets to the plain text with a single command:

kubectl get secret shop-db -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

So why a separate object at all? Kubernetes treats a Secret differently: it only reaches the node where a Pod needs it, and mounted as a volume the kubelet keeps it in a tmpfs in memory rather than on disk, disappearing once no Pod references it. Separate RBAC permissions and encrypted storage come on top, neither of them automatically.

You can also create a Secret imperatively instead of via manifest, useful in a CI pipeline when the password comes from a vault at runtime and should never end up in a file:

kubectl create secret generic shop-db \
  --from-literal=DB_USER=shop \
  --from-literal=DB_PASSWORD='just-an-example'

Inside the Pod you wire up Secrets exactly like ConfigMaps, just with references named secretKeyRef and secretRef, and secret with secretName for volumes. I prefer the volume for Secrets: environment variables get inherited by every child process and show up in crash dumps, while a file under /etc/secrets only gets read by whoever opens it deliberately.

The type field tells Kubernetes and your colleagues what a Secret holds. The most important types:

Type For
Opaque default, arbitrary key-value pairs
kubernetes.io/tls certificate and private key, for example for an Ingress
kubernetes.io/dockerconfigjson credentials for a private registry, as an imagePullSecret on the Pod
kubernetes.io/basic-auth username and password, in the keys username and password
kubernetes.io/service-account-token a service account's token for the Kubernetes API

Kubernetes ConfigMap vs. Secret: the difference at a glance

ConfigMap Secret
Content configuration anyone on the team may see passwords, tokens, certificates, registry credentials
Data fields data, binaryData data (base64), stringData (plain text)
Type none type, default Opaque
Storage in etcd plain text plain text, until you enable encryption at rest
Distribution to nodes like any object only to nodes with a Pod that needs it, kept there in tmpfs
Mounting into the Pod env, envFrom, volume env, envFrom, volume, imagePullSecrets
Size limit 1 MiB 1 MiB
Immutable option yes, with immutable: true yes, with immutable: true

The decision is simple: anything readable by anyone in a pull request is a ConfigMap. Anything you would not post in a team chat is a Secret. When in doubt, use the Secret; the effort is the same, and you can protect it later with RBAC and encryption, which you cannot do for a ConfigMap.

What updates automatically when you change something

The most common question I get about ConfigMaps: I changed the value, why does my Pod still see the old one? The answer depends on how the data got into the Pod.

Mounted as a volume, the kubelet updates the files on its own, checking periodically whether the object changed and swapping the files, without a restart. Your application still has to reread the file though; one reading the configuration only at startup notices nothing. Exception: mounts with subPath never get updates.

As an environment variable there is no update at all. A process gets its environment at startup and keeps it, so only a new Pod sees the new value. In practice that means running kubectl rollout restart deployment <name> after a change, or triggering the rollout automatically with a hash of the ConfigMap as an annotation on the Pod template, as many Helm charts do. The side effect is welcome: a broken value surfaces during the readiness check before it reaches every Pod.

To prevent changes outright, set immutable: true. An immutable ConfigMap or Secret can only be deleted and recreated, and the kubelet no longer has to watch it for changes, which reduces load on the API server with many Pods. I use this for configuration regenerated per release with a versioned name.

Encryption at rest, RBAC and audit: locking down Secrets in the cluster

By default a Secret sits in etcd in plain text, just like a ConfigMap. Anyone who gets hold of an etcd backup or direct database access reads every Secret in the cluster. Three measures belong in any cluster that is more than a playground.

First, encryption at rest: the API server encrypts Secrets before writing them to etcd, controlled through an EncryptionConfiguration:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: "BASE64_ENCODED_32_BYTE_KEY"
      - identity: {}

You pass this file to the kube-apiserver via the --encryption-provider-config parameter. The first provider in the list is used for writing, all the others for reading; identity stands for unencrypted and keeps existing Secrets readable. Existing Secrets only get encrypted the next time they are written, so you rewrite them all once:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

The key here sits on the control plane node next to the configuration, protecting against copied backups and direct etcd access, not against root on the control plane; for more, attach an external key service through the KMS provider. On managed clusters, encryption at rest is usually a switch or already on, on k3s a server startup parameter. Full instructions are in the Kubernetes documentation on encryption at rest.

Second, RBAC. Whoever has get, list or watch on Secrets reads the contents; list alone is enough, since the list includes the data. Grant these rights only to accounts and people who need them, separately from ConfigMaps. Keep in mind too: whoever can create Pods in a namespace can mount any Secret there and read it, so Pod permissions are Secret permissions. How to cut roles cleanly is covered in Kubernetes RBAC: roles and permissions.

Third, the audit view: I want to answer who read which Secret and when. The API server's audit log delivers that once you enable the Metadata level for Secrets; the contents must never go into the log. One detail surprises people: a container with privileged: true can read every Secret in use on its node, so banning privileged Pods is Secret protection too.

Secrets in Git: SOPS, Sealed Secrets and External Secrets Operator

If you run Kubernetes declaratively, you also want to version Secrets in Git. A Secret manifest with stringData should never go into a repository unencrypted, and a base64-encoded data field is, as shown above, just as much plain text. Three approaches have proven themselves in my world.

Approach How it works What it fits
SOPS encrypts manifest values with age, PGP or a cloud KMS; keys stay readable, decryption happens at rollout GitOps teams reviewing Secrets in pull requests without an external vault
Sealed Secrets a cluster controller holds a key pair; kubeseal encrypts against the public key, producing a SealedSecret that becomes a Secret in the cluster teams with one cluster keeping everything inside Kubernetes
External Secrets Operator an operator syncs secrets from AWS Secrets Manager, HashiCorp Vault, Azure Key Vault and others into Kubernetes Secrets; only the reference lives in Git companies already running a central vault for several clusters

In my clusters I use SOPS. The Secret manifest, with encrypted values, sits in the same repository as the rest of the infrastructure, the key names stay readable, and in a pull request I can see which Secret changed without seeing the content. The private key never lives in the repository. On rollout through ArgoCD it gets decrypted, and an ordinary Kubernetes Secret appears in the cluster, the path with the least effort for one or two clusters. You can find the tool on GitHub under getsops/sops.

Setting up a Vault cluster just to inject passwords is over-engineering in my view. The External Secrets Operator is worth it once a vault already exists and supplies several systems: it becomes the single source of truth, Kubernetes only gets a copy. What does not work is two sources at once, and I have seen teams keep Secrets in a cloud vault and through SOPS in Git until nobody knew which version was authoritative.

Frequently asked questions

Are Kubernetes Secrets secure?

Only to a limited extent out of the box. Base64 is encoding, not encryption. A Secret becomes secure only through the three things above: encryption at rest, tight RBAC rules and a ban on privileged containers.

ConfigMap or Secret: which do I use for a database URL?

It depends on whether the password is part of the URL. Host, port and database name are configuration and belong in the ConfigMap. Once username and password are part of the URL, the whole value is a Secret. Cleaner still is assembling the URL inside the application from both sources: host from the ConfigMap, credentials from the Secret.

Why does my Pod not see the changed ConfigMap?

Because the data is probably mounted as environment variables, which only get set when the process starts and never get updated; only a new Pod sees the new value. A kubectl rollout restart fixes that. With volumes, the kubelet updates the files on its own after a short delay, except for mounts with subPath and objects marked immutable: true.

How do I get Secrets into Git without storing them in plain text?

With a tool that encrypts the values before the commit: SOPS directly in the manifest, Sealed Secrets as an encrypted object for a cluster controller. If you already run a vault, use the External Secrets Operator.

How large can a ConfigMap be?

At most 1 MiB, the same limit applies to Secrets, which is plenty for configuration files, certificates and feature flags. Anyone hitting the limit usually has data that belongs in a volume or a database instead, models, images or large datasets, for example.

Where to go next

Two related topics follow from here. How you write and validate manifests cleanly before they go into the cluster is covered in Kubernetes YAML: understanding manifests. And since Pod permissions are Secret permissions, Kubernetes RBAC: roles and permissions is the logical next read. The concept chapter on Secrets in the Kubernetes documentation lists every type and security note there was no room for here.

My suggestion for today: run kubectl get secrets --all-namespaces and check your Git repository for a password sitting anywhere in plain text or merely base64-encoded. If you find one, SOPS is the quickest way out, and encryption at rest in the cluster is the next step after that.

In full detail, from volume mounts through registry secrets to reading Secrets via the Kubernetes API, this is covered in chapter 3 of my Kubernetes Practical Guide.

Kevin Welter

Kevin Welter

Developer, IT architect, author of technical books (Kubernetes, cloud infrastructures) and speaker. Runs his business with an AI workforce of eight AI employees and shows solo business owners in his community how to hire their first AI employee.

More about AI employees

Kubernetes from the basics to a production-ready cluster

Get the book