Storage

ConfigMaps & Secrets

● Intermediate ⏱ 12 min read

Baking configuration into container images creates a new image for every environment change. Kubernetes separates config from code with two resources: ConfigMaps for non-sensitive configuration (database hostnames, feature flags, nginx configs) and Secrets for sensitive data (passwords, API keys, TLS certificates). Both can be injected into pods as environment variables or as files in the filesystem.

ConfigMaps

A ConfigMap stores key-value pairs. Values can be short strings (a port number, a log level) or entire file contents (an nginx config, a properties file). They're namespace-scoped and unencrypted.

Creating ConfigMaps

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
data:
  # Simple key-value pairs
  LOG_LEVEL: info
  DATABASE_HOST: postgres.production.svc.cluster.local
  DATABASE_PORT: "5432"
  FEATURE_DARK_MODE: "true"

  # File content stored as a value (key = filename)
  nginx.conf: |
    server {
      listen 80;
      location /health { return 200 "ok"; }
    }
imperative creation
# From literal values
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=DATABASE_HOST=postgres.production

# From a file (key = filename, value = file contents)
kubectl create configmap nginx-config \
  --from-file=nginx.conf

# From an env file (KEY=VALUE format, one per line)
kubectl create configmap app-config \
  --from-env-file=config.env

Consuming ConfigMaps

Three ways to consume a ConfigMap in a pod:

configmap-consumption.yaml
spec:
  containers:
  - name: app
    image: my-app:latest

    # 1. Individual env vars
    env:
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: LOG_LEVEL

    # 2. All keys as env vars at once (envFrom)
    envFrom:
    - configMapRef:
        name: app-config   # every key becomes an env var

    # 3. As files in the filesystem (via volume)
    volumeMounts:
    - name: nginx-conf
      mountPath: /etc/nginx/conf.d
      readOnly: true

  volumes:
  - name: nginx-conf
    configMap:
      name: nginx-config    # each key becomes a file

Secrets

Secrets have the same structure as ConfigMaps but are intended for sensitive values. Data is stored base64-encoded in etcd. The API returns base64 — you must decode to get the raw value.

secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
data:
  # Values must be base64-encoded
  username: cG9zdGdyZXM=     # echo -n "postgres" | base64
  password: c3VwZXJzZWNyZXQ=  # echo -n "supersecret" | base64
stringData:
  # Alternative: plain text in stringData (Kubernetes base64-encodes on apply)
  api-key: my-raw-api-key-here
imperative creation
# From literals (values stored as-is, no manual base64)
kubectl create secret generic db-credentials \
  --from-literal=username=postgres \
  --from-literal=password=supersecret

# TLS Secret from cert files
kubectl create secret tls my-tls \
  --cert=tls.crt \
  --key=tls.key

# Docker registry credentials
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass

Secret Types

TypeUseRequired keys
OpaqueArbitrary user-defined data (default)Any
kubernetes.io/tlsTLS certificate and keytls.crt, tls.key
kubernetes.io/dockerconfigjsonContainer registry credentials.dockerconfigjson
kubernetes.io/service-account-tokenServiceAccount bearer tokenAuto-generated by Kubernetes
kubernetes.io/basic-authHTTP Basic Auth credentialsusername, password
kubernetes.io/ssh-authSSH private keyssh-privatekey

Consuming Secrets

The consumption API is identical to ConfigMaps — env vars, envFrom, or volumes. Volume mounts are preferred for credentials: they don't appear in kubectl describe pod output and can be updated without restarting the pod.

spec:
  containers:
  - name: app
    image: my-app:latest

    # Env var from a specific Secret key
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password

    # All Secret keys as env vars
    envFrom:
    - secretRef:
        name: db-credentials

    # As files (volume mount — preferred for creds)
    volumeMounts:
    - name: db-creds
      mountPath: /run/secrets/db
      readOnly: true

  volumes:
  - name: db-creds
    secret:
      secretName: db-credentials
      defaultMode: 0400   # owner read-only

Why Secrets Aren't Secret

Where Secret data lives (and who can read it)
etcd
Base64-encoded by default — not encrypted. Anyone with etcd access reads raw values. Enable EncryptionConfiguration with AES-GCM or KMS provider for encryption at rest.
API server
Any principal with get secrets RBAC permission reads them. Scope RBAC tightly — list secrets alone returns all values.
node
Kubelet caches Secrets on disk (tmpfs) for pods running on that node. Node root access = Secret access.
env var
Visible in /proc/<pid>/environ, crash dumps, container inspection. Prefer volume mounts for secrets.
Hardening checklist: enable etcd encryption at rest · restrict RBAC to need-to-know · use external secrets (ESO, Vault, ASM) · avoid printing secrets in logs
Kubernetes Secrets offer API-level separation, not cryptographic secrecy. Layer defense: encrypt etcd, restrict RBAC, use external secret stores.

Live Update Behavior

Updates to ConfigMaps and Secrets propagate differently depending on how they're consumed:

Consumption methodUpdates automatically?Delay
Volume mount (full directory)YesKubelet sync period (default ~60s) + propagation time
Volume mount with subPathNoRequires pod restart
Environment variable (env)NoRequires pod restart
Environment variable (envFrom)NoRequires pod restart

For applications that watch the filesystem for config changes (nginx, Prometheus, most 12-factor apps), volume mounts give zero-downtime config updates — the app reloads when it detects file changes. Environment variables require a rolling restart.

External Secrets

For production, store secret values outside the cluster in a dedicated secrets manager and sync them into Kubernetes Secrets automatically. This avoids committing secrets to Git and centralizes rotation:

ToolBackendHow it works
External Secrets Operator (ESO)AWS Secrets Manager, GCP Secret Manager, Vault, Azure Key Vault, 1PasswordCRD-based sync — ExternalSecret polls the backend and creates/updates a Kubernetes Secret
Vault Agent InjectorHashiCorp VaultSidecar injected by a mutating webhook writes secrets to a shared volume at pod start
Secrets Store CSI DriverAWS, GCP, Azure, VaultMounts secrets directly from the provider as a volume — no Kubernetes Secret created

kubectl Commands

# List ConfigMaps
kubectl get configmap -n production
kubectl get cm -n production   # shorthand

# View ConfigMap data
kubectl get cm app-config -n production -o yaml

# Edit a ConfigMap in-place
kubectl edit cm app-config -n production

# List Secrets (data is not shown in output)
kubectl get secret -n production

# Decode a Secret value
kubectl get secret db-credentials -n production \
  -o jsonpath='{.data.password}' | base64 -d

# Compare the current ConfigMap to what's mounted in a running pod
kubectl exec deploy/app -n production -- cat /etc/config/app.properties
kubectl get cm app-config -n production -o jsonpath='{.data.app\.properties}'

# Delete and recreate a ConfigMap (to force a volume update)
kubectl delete cm app-config -n production
kubectl apply -f app-config.yaml