Kubernetes ConfigMaps and Secrets 2025 — Configuration Management Done Right

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Hardcoding configuration into container images violates the twelve-factor app methodology and makes multi-environment deployments impossible. A database URL, API key, or feature flag that differs between staging and production cannot be baked into the image — it must be injected at runtime.

Kubernetes provides two resources for this: ConfigMaps for non-sensitive configuration data and Secrets for sensitive data. Both decouple application configuration from the container image, enabling the same image to run in development, staging, and production with different configurations.

However, Kubernetes Secrets are not truly secure out of the box — they are stored as base64-encoded (not encrypted) data in etcd by default. Production teams need to understand this limitation and implement external secret management to address it.

ConfigMaps

ConfigMaps store non-sensitive key-value pairs or entire configuration files.

Creating ConfigMaps

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
  namespace: production
data:
  # Simple key-value pairs
  LOG_LEVEL: "info"
  PORT: "3000"
  NODE_ENV: "production"
  CACHE_TTL: "3600"
 
  # Entire config file as a value
  nginx.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://localhost:3000;
      }
    }
 
  app-settings.json: |
    {
      "maxConnections": 100,
      "timeout": 30,
      "retries": 3
    }
# Create from YAML
kubectl apply -f configmap.yaml
 
# Create from literal values
kubectl create configmap api-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=PORT=3000 \
  -n production
 
# Create from a file (file name becomes the key)
kubectl create configmap nginx-config \
  --from-file=nginx.conf \
  -n production
 
# Create from a directory (all files become keys)
kubectl create configmap app-config \
  --from-file=./config/ \
  -n production
 
# View
kubectl get configmap api-config -n production -o yaml
kubectl describe configmap api-config -n production

Consuming ConfigMaps as Environment Variables

spec:
  containers:
  - name: api
    image: myrepo/api:1.0
    # Load all keys as environment variables
    envFrom:
    - configMapRef:
        name: api-config
    # Or load specific keys
    env:
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: api-config
          key: LOG_LEVEL
    - name: APP_PORT
      valueFrom:
        configMapKeyRef:
          name: api-config
          key: PORT

Consuming ConfigMaps as Volume Mounts (Files)

spec:
  volumes:
  - name: nginx-config-vol
    configMap:
      name: nginx-config
  - name: app-settings-vol
    configMap:
      name: api-config
      items:
      - key: app-settings.json
        path: settings.json  # mount as /config/settings.json
 
  containers:
  - name: nginx
    image: nginx:alpine
    volumeMounts:
    - name: nginx-config-vol
      mountPath: /etc/nginx/conf.d  # all keys become files here
      readOnly: true
  - name: api
    image: myrepo/api:1.0
    volumeMounts:
    - name: app-settings-vol
      mountPath: /app/config
      readOnly: true

Kubernetes automatically updates mounted ConfigMap files when the ConfigMap changes — without restarting the Pod. Environment variables injected via envFrom require a Pod restart to pick up changes.

Secrets

Secrets work like ConfigMaps but are intended for sensitive data. They are stored in etcd and transmitted to nodes only when a Pod on that node requires them.

Creating Secrets

# secret.yaml — use stringData for plain text (auto-encoded to base64)
apiVersion: v1
kind: Secret
metadata:
  name: api-secrets
  namespace: production
type: Opaque
stringData:
  DATABASE_URL: "postgres://app:s3cr3t@db.cluster.svc:5432/mydb"
  JWT_SECRET: "my-very-long-random-secret-key-for-jwt-signing"
  STRIPE_API_KEY: "sk_live_xxxxxxxxxxxxxxxxxxxxxx"
# Create from literal (values are auto-base64-encoded)
kubectl create secret generic api-secrets \
  --from-literal=DATABASE_URL='postgres://app:pass@db:5432/mydb' \
  --from-literal=JWT_SECRET='supersecretkey' \
  -n production
 
# Create TLS secret (for Ingress TLS)
kubectl create secret tls my-tls-secret \
  --cert=tls.crt \
  --key=tls.key \
  -n production
 
# Create Docker registry credentials secret
kubectl create secret docker-registry regcred \
  --docker-server=123456789.dkr.ecr.us-east-1.amazonaws.com \
  --docker-username=AWS \
  --docker-password=$(aws ecr get-login-password --region us-east-1) \
  -n production
 
# View (base64 encoded)
kubectl get secret api-secrets -n production -o yaml
 
# Decode a specific key
kubectl get secret api-secrets -n production \
  -o jsonpath='{.data.DATABASE_URL}' | base64 -d

Consuming Secrets in Pods

spec:
  containers:
  - name: api
    image: myrepo/api:1.0
    # Load all secret keys as environment variables
    envFrom:
    - secretRef:
        name: api-secrets
    # Reference specific secret keys
    env:
    - name: DB_URL
      valueFrom:
        secretKeyRef:
          name: api-secrets
          key: DATABASE_URL
 
  # Reference Docker registry secret for private images
  imagePullSecrets:
  - name: regcred

Mounting Secrets as Files

Better than environment variables for secrets — files are not visible in ps aux or crash dumps:

spec:
  volumes:
  - name: secrets-vol
    secret:
      secretName: api-secrets
      defaultMode: 0400  # read-only by owner
 
  containers:
  - name: api
    volumeMounts:
    - name: secrets-vol
      mountPath: /run/secrets
      readOnly: true
    # Access: cat /run/secrets/DATABASE_URL

The Secret Security Problem

Kubernetes Secrets are base64-encoded, not encrypted. By default, they are stored in plain text in etcd. Anyone with etcd access or the ability to kubectl get secret can read them.

Enable encryption at rest (etcd-level encryption):

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: <base64-encoded-32-byte-key>
  - identity: {}

External Secrets Operators (Production Standard)

For production, store secrets in dedicated secret managers and sync them to Kubernetes Secrets:

External Secrets Operator (ESO)

# SecretStore points to AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secretsmanager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        secretRef:
          accessKeyIDSecretRef:
            name: aws-credentials
            key: access-key-id
          secretAccessKeySecretRef:
            name: aws-credentials
            key: secret-access-key
 
---
# ExternalSecret syncs from Secrets Manager to K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: api-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secretsmanager
    kind: SecretStore
  target:
    name: api-secrets        # name of the K8s Secret to create
    creationPolicy: Owner
  data:
  - secretKey: DATABASE_URL  # key in K8s Secret
    remoteRef:
      key: prod/api/database  # path in Secrets Manager
      property: url
  - secretKey: JWT_SECRET
    remoteRef:
      key: prod/api/jwt
      property: secret

ESO supports AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, and more.

Sealed Secrets (GitOps Approach)

Sealed Secrets encrypt Kubernetes Secrets so they can be committed to Git:

# Install kubeseal CLI
brew install kubeseal
 
# Seal a secret (encrypted with cluster's public key)
kubectl create secret generic api-secrets \
  --from-literal=DATABASE_URL='postgres://...' \
  --dry-run=client -o yaml | \
  kubeseal --format yaml > sealed-secret.yaml
 
# Commit sealed-secret.yaml to Git safely — only the cluster can decrypt
kubectl apply -f sealed-secret.yaml

RBAC for Secret Access

Restrict which service accounts can read Secrets:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["api-secrets"]  # specific secret only
  verbs: ["get"]
 
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: api-secret-binding
  namespace: production
subjects:
- kind: ServiceAccount
  name: api-service-account
  namespace: production
roleRef:
  kind: Role
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io

Common Mistakes

  • Using Secrets for non-sensitive config (wastes Secret quota and RBAC complexity) — use ConfigMaps for LOG_LEVEL, PORT, etc.
  • Assuming Kubernetes Secrets are encrypted — base64 encoding is not encryption; enable etcd encryption or use ESO
  • Committing unencrypted secret manifests to Git — use Sealed Secrets or external secret managers
  • Using envFrom for all config including secrets — env vars appear in crash reports and kubectl describe pod
  • Not setting RBAC restrictions on Secrets — any pod in the namespace can read all secrets by default in many clusters

Best Practices

  • Use ConfigMaps for app configuration (log levels, feature flags, ports) and Secrets for credentials and keys
  • Mount Secrets as files in /run/secrets rather than environment variables — files are not captured in crash dumps
  • Use External Secrets Operator in production to sync from AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault
  • Enable etcd encryption at rest on self-managed clusters
  • Scope RBAC to specific Secret names, not all secrets in a namespace
  • Rotate secrets regularly — ESO's refreshInterval enables automatic rotation pickup

Key Takeaways

  • ConfigMaps store non-sensitive config as key-value pairs or file contents; Secrets store sensitive data like passwords and tokens
  • ConfigMaps mounted as volumes update automatically without pod restart; environment variable ConfigMaps require pod restart
  • Kubernetes Secrets are base64-encoded (not encrypted) by default — enable etcd encryption at rest or use external secret managers
  • External Secrets Operator syncs from AWS Secrets Manager, GCP Secret Manager, or Vault into Kubernetes Secrets automatically
  • Sealed Secrets encrypt secrets so they can be stored safely in Git — the cluster holds the private decryption key
  • Mount secrets as files (/run/secrets/) rather than environment variables to prevent exposure in process listings and crash logs
  • Use RBAC to restrict secret access to specific ServiceAccounts and specific Secret names, not all secrets in a namespace
  • Secret rotation should be automated — manual rotation is a toil that gets skipped under pressure

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading