Kubernetes ConfigMaps and Secrets: Configuration Management (2026)

Configuration management is one of the most critical—and most frequently mishandled—aspects of running workloads on Kubernetes. ConfigMaps handle non-sensitive configuration; Secrets handle credentials. But in a production cluster, you need more: external secret stores, encrypted storage, automatic rotation, and GitOps-safe patterns. This guide covers the full spectrum from basic kubectl commands to HashiCorp Vault sidecar injection.

Creating ConfigMaps

A ConfigMap is a namespaced key-value store for non-confidential data. You can create one from literals, from files, or from an env file — each approach suits a different workflow.

From Literals

# Single command, multiple keys
kubectl create configmap app-config \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=info \
  --from-literal=MAX_CONNECTIONS=100

# Verify
kubectl get configmap app-config -o yaml

From a File

# Create a properties file first
cat > app.properties <<EOF
database.url=jdbc:postgresql://postgres:5432/mydb
database.pool.size=10
cache.ttl.seconds=300
EOF

# ConfigMap key = filename, value = file content
kubectl create configmap app-props \
  --from-file=app.properties

# Or rename the key
kubectl create configmap app-props \
  --from-file=config=app.properties

From an Env File

# .env style file — each line becomes a separate key
cat > prod.env <<EOF
DB_HOST=postgres.default.svc.cluster.local
DB_PORT=5432
REDIS_HOST=redis.default.svc.cluster.local
EOF

kubectl create configmap prod-env-config \
  --from-env-file=prod.env

The --from-env-file flag differs from --from-file: each line in the env file becomes a separate key in the ConfigMap rather than storing the entire file under one key.

Declarative YAML (Preferred for GitOps)

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
data:
  APP_ENV: "production"
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  app.properties: |
    database.url=jdbc:postgresql://postgres:5432/mydb
    database.pool.size=10
    cache.ttl.seconds=300
Pro Tip: Keep ConfigMap YAML files in your Git repository alongside your Deployment manifests. This makes environment-specific configuration auditable and reviewable in PRs.

Mounting ConfigMaps: Env Vars vs Volume Files

ConfigMaps can surface data to pods in two ways: as environment variables or as mounted files. The right choice depends on how your application reads configuration.

As Environment Variables

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    spec:
      containers:
      - name: app
        image: my-app:1.4.2
        # Inject a single key
        env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: LOG_LEVEL
        # Inject all keys from a ConfigMap
        envFrom:
        - configMapRef:
            name: app-config

As Volume Files

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      volumes:
      - name: config-volume
        configMap:
          name: app-config
          # Optionally set file permissions
          defaultMode: 0644
          # Mount only specific keys
          items:
          - key: app.properties
            path: application.properties
      containers:
      - name: app
        image: my-app:1.4.2
        volumeMounts:
        - name: config-volume
          mountPath: /etc/config
          readOnly: true
Note: Volume-mounted ConfigMaps update automatically when the ConfigMap changes (with a ~60s propagation delay). Environment variables from ConfigMaps do NOT hot-reload — a pod restart is required. For dynamic configuration, prefer volume mounts and use inotify or polling in your application.

Kubernetes Secret Types

Kubernetes has several built-in Secret types. Using the correct type enables type-specific validation and allows tools to interpret the secret correctly.

TypeUse CaseRequired Keys
OpaqueArbitrary user-defined data (passwords, API keys)Any
kubernetes.io/tlsTLS certificate + private keytls.crt, tls.key
kubernetes.io/dockerconfigjsonDocker registry credentials (pull secrets).dockerconfigjson
kubernetes.io/service-account-tokenService account tokens (auto-created)token, ca.crt
kubernetes.io/basic-authHTTP basic auth credentialsusername, password
kubernetes.io/ssh-authSSH private keyssh-privatekey

Creating Secrets by Type

# Opaque secret (generic)
kubectl create secret generic db-credentials \
  --from-literal=username=myuser \
  --from-literal=password='S3cur3P@ssw0rd!'

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

# Docker registry pull secret
kubectl create secret docker-registry registry-credentials \
  --docker-server=registry.example.com \
  --docker-username=ci-user \
  --docker-password=mytoken \
  --docker-email=ci@example.com

Base64 Encoding and Decoding

Secret values in YAML manifests must be base64-encoded. This is encoding, not encryption — anyone with access to the manifest can decode it trivially.

# Encode a value
echo -n 'S3cur3P@ssw0rd!' | base64
# Output: UzNjdXIzUEBzc3cwcmQh

# Decode
echo 'UzNjdXIzUEBzc3cwcmQh' | base64 --decode
# Output: S3cur3P@ssw0rd!

# On macOS, use base64 -D to decode
echo 'UzNjdXIzUEBzc3cwcmQh' | base64 -D
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
data:
  # Values must be base64-encoded
  username: bXl1c2Vy          # myuser
  password: UzNjdXIzUEBzc3cwcmQh  # S3cur3P@ssw0rd!
Pro Tip: Use stringData instead of data in your YAML to write plaintext values — Kubernetes encodes them automatically at write time. This is safer for generation scripts, but never commit plaintext Secret manifests to Git.

External Secrets Operator with AWS Secrets Manager

The External Secrets Operator (ESO) syncs secrets from external stores (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault) into Kubernetes Secrets. This keeps your actual secret values out of your cluster entirely.

# Install ESO via Helm
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets \
  --create-namespace \
  --set installCRDs=true

Create a SecretStore that references your AWS Secrets Manager:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        # Use IRSA (IAM Roles for Service Accounts) — preferred
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

Then create an ExternalSecret that pulls a specific secret:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
    template:
      type: Opaque
  data:
  - secretKey: username
    remoteRef:
      key: production/myapp/db
      property: username
  - secretKey: password
    remoteRef:
      key: production/myapp/db
      property: password

ESO creates and keeps the Kubernetes Secret in sync with the external store. When you rotate the secret in AWS Secrets Manager, ESO picks it up within the refreshInterval.

Sealed Secrets with kubeseal

Sealed Secrets (by Bitnami) encrypt Kubernetes Secrets so they can be safely committed to Git. Only the SealedSecrets controller in your cluster can decrypt them.

# Install the controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
  -n kube-system

# Install kubeseal CLI
brew install kubeseal   # macOS
# Or download binary from GitHub releases

# Fetch the public key (optional, for offline sealing)
kubeseal --fetch-cert \
  --controller-name=sealed-secrets \
  --controller-namespace=kube-system \
  > pub-cert.pem
# Create a regular Secret manifest (don't apply it!)
kubectl create secret generic db-credentials \
  --from-literal=username=myuser \
  --from-literal=password='S3cur3P@ssw0rd!' \
  --dry-run=client -o yaml > secret.yaml

# Seal it — output is safe to commit to Git
kubeseal --format yaml \
  --cert pub-cert.pem \
  < secret.yaml > sealed-secret.yaml

# Apply the SealedSecret — the controller decrypts and creates the Secret
kubectl apply -f sealed-secret.yaml
# sealed-secret.yaml looks like this (safe for Git)
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  encryptedData:
    password: AgBy8hCa...  # encrypted blob
    username: AgCE9xKb...  # encrypted blob
  template:
    metadata:
      name: db-credentials
      namespace: production
    type: Opaque

HashiCorp Vault + Vault Agent Injector

The Vault Agent Injector uses a mutating admission webhook to automatically inject a Vault Agent sidecar into pods. The sidecar authenticates to Vault, retrieves secrets, and writes them to a shared in-memory volume. No application code changes required.

# Install Vault with injector enabled (dev mode for demo)
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
  --set "server.dev.enabled=true" \
  --set "injector.enabled=true" \
  -n vault --create-namespace

# Enable Kubernetes auth method
kubectl exec -n vault vault-0 -- vault auth enable kubernetes

# Configure it
kubectl exec -n vault vault-0 -- vault write auth/kubernetes/config \
  kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

# Create a policy
kubectl exec -n vault vault-0 -- vault policy write myapp-policy - <<EOF
path "secret/data/production/myapp/*" {
  capabilities = ["read"]
}
EOF

# Create a role binding the K8s service account to the policy
kubectl exec -n vault vault-0 -- vault write auth/kubernetes/role/myapp \
  bound_service_account_names=myapp-sa \
  bound_service_account_namespaces=production \
  policies=myapp-policy \
  ttl=1h

Annotate your Deployment to trigger the injector:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: production
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "myapp"
        # Inject secret at /vault/secrets/db-creds
        vault.hashicorp.com/agent-inject-secret-db-creds: "secret/data/production/myapp/db"
        # Template the secret file format
        vault.hashicorp.com/agent-inject-template-db-creds: |
          {{- with secret "secret/data/production/myapp/db" -}}
          export DB_USERNAME="{{ .Data.data.username }}"
          export DB_PASSWORD="{{ .Data.data.password }}"
          {{- end }}
    spec:
      serviceAccountName: myapp-sa
      containers:
      - name: app
        image: my-app:1.4.2
        command: ["/bin/sh", "-c"]
        args:
        - |
          source /vault/secrets/db-creds
          exec java -jar /app/app.jar

Secret Rotation Strategies

Secrets must be rotated regularly and immediately after any suspected exposure. The strategy depends on your secret management approach.

Strategy 1: Rolling Restart After ESO Sync

# After rotating secret in AWS Secrets Manager, force ESO to sync immediately
kubectl annotate externalsecret db-credentials \
  force-sync=$(date +%s) \
  --overwrite

# Then trigger rolling restart of affected deployments
kubectl rollout restart deployment/my-app -n production

Strategy 2: Versioned Secrets

# Keep current and previous versions to allow zero-downtime rotation
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials-v2   # New version
  namespace: production
type: Opaque
stringData:
  username: myuser
  password: NewS3cur3P@ss!
# Update Deployment to reference new secret, then delete old
kubectl set env deployment/my-app \
  --from=secret/db-credentials-v2 \
  -n production

# Monitor rollout
kubectl rollout status deployment/my-app -n production

# Once successful, delete old secret
kubectl delete secret db-credentials-v1 -n production
Note: For database credentials, use a dual-write rotation pattern: create new credentials in the DB, update the secret, wait for all pods to pick up the new credentials, then revoke the old credentials. This ensures zero-downtime rotation.

Frequently Asked Questions

Are Kubernetes Secrets actually encrypted?

By default, Secrets are only base64-encoded in the API server's storage (etcd) — not encrypted. To enable encryption at rest, you must configure an EncryptionConfiguration resource and restart the API server. In managed Kubernetes (EKS, GKE, AKS), etcd encryption is typically enabled by default, but check your provider's documentation. Consider ESO or Vault for truly external secret storage.

What's the difference between ConfigMap and Secret for non-sensitive config?

Use ConfigMaps for non-sensitive data (feature flags, log levels, connection pool sizes, static configuration files). Use Secrets for anything that grants access or proves identity: passwords, API keys, certificates, tokens. Secrets have restricted RBAC by default and are stored in a separate etcd keyspace, making them eligible for encryption at rest.

Can I limit which pods can access a Secret?

Yes, via RBAC. Create a Role that grants get on specific Secrets, bind it to the pod's ServiceAccount. Also use admission controllers (OPA Gatekeeper or Kyverno) to enforce that only specific service accounts can reference specific Secrets. Network-level isolation with NetworkPolicies does not protect Secrets — that's an API server concern, not a network concern.

How large can a ConfigMap or Secret be?

Both are limited to 1 MiB (1,048,576 bytes) total size. This covers all keys and values combined. If you need to inject larger files (e.g., ML model configs, large certificates bundles), consider using a volume mounted from a PersistentVolume, an init container that downloads the file, or an external object store (S3) with a sidecar.

When should I use Sealed Secrets vs External Secrets Operator?

Use Sealed Secrets when your threat model centers on Git repository security and you want a lightweight, fully self-contained solution. Use External Secrets Operator when you already have an enterprise secret store (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) and want a single source of truth for secrets across cloud and Kubernetes workloads. ESO is the better choice for multi-team, multi-cluster environments.