← Back to Blog

Kubernetes ConfigMaps & Secrets: Complete Guide

Hardcoding configuration in container images is the fastest way to create a deployment nightmare - separate builds for every environment, leaked credentials in Git history, and no way to update config without rebuilding. ConfigMaps and Secrets solve this by injecting configuration at runtime, keeping your images environment-agnostic and your secrets out of source control.

ConfigMap vs. Secret: The Core Distinction

Both ConfigMaps and Secrets store key-value pairs and inject them into Pods. The difference is how Kubernetes treats the data:

  • ConfigMap - stores non-sensitive configuration data as plain text. Log levels, feature flags, service URLs, database hostnames.
  • Secret - stores sensitive data. Values are base64-encoded (not encrypted by default, but RBAC and etcd encryption can protect them). Passwords, API keys, TLS certificates, connection strings.

The functional difference is minimal - both can be mounted as files or injected as environment variables. The important difference is access control and audit logging. Secrets can be restricted with RBAC policies, encrypted at rest in etcd, and tracked separately in audit logs.

Creating ConfigMaps

From a YAML file (recommended for GitOps)

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_FLAGS: "dark-mode=true,beta-api=false"

  # Multi-line value (e.g., an nginx config file)
  nginx.conf: |
    server {
      listen 80;
      location /health {
        return 200 'ok';
      }
    }

From the command line (quick, not recommended for production)

# From literal values
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=DATABASE_HOST=postgres

# From an existing file
kubectl create configmap nginx-config \
  --from-file=nginx.conf=./nginx.conf

# From a directory (creates one key per file)
kubectl create configmap app-config \
  --from-file=./config/

Creating Secrets

Using stringData (Kubernetes base64-encodes it for you)

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: production
type: Opaque
stringData:
  DATABASE_PASSWORD: "my-actual-password"
  API_KEY: "sk-1234567890abcdef"
  JWT_SECRET: "a-long-random-string-at-least-32-chars"

Using data (values must be pre-encoded as base64)

# Encode a value
echo -n "my-actual-password" | base64
# Output: bXktYWN0dWFsLXBhc3N3b3Jk

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: production
type: Opaque
data:
  DATABASE_PASSWORD: bXktYWN0dWFsLXBhc3N3b3Jk

Use our free Base64 Encoder to encode your secret values without leaving the browser. Values are never sent to a server.

TLS Secrets

# Create a TLS secret from cert and key files
kubectl create secret tls my-tls-secret \
  --cert=tls.crt \
  --key=tls.key \
  -n production

Method 1: Inject as Environment Variables

This is the simplest approach. Individual keys become environment variables inside the container:

spec:
  containers:
  - name: api
    image: myorg/api:v1.2.3
    env:
    # From ConfigMap
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: LOG_LEVEL
    # From Secret
    - name: DATABASE_PASSWORD
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: DATABASE_PASSWORD

Inject all keys at once with envFrom

If you have many variables, envFrom is cleaner - it injects all keys from the ConfigMap or Secret as environment variables:

spec:
  containers:
  - name: api
    image: myorg/api:v1.2.3
    envFrom:
    - configMapRef:
        name: app-config
    - secretRef:
        name: app-secrets

Be careful with envFrom: all keys in the ConfigMap/Secret become env vars, which can pollute the environment. Prefer explicit env references for production to avoid accidentally exposing keys your application does not need.

Method 2: Mount as Files

Mounting as files is better for large configuration blobs (nginx.conf, application.yaml), and for cases where the application reads config from files rather than environment variables. Mounted files update automatically when the ConfigMap changes (with a short delay), without requiring a Pod restart.

spec:
  containers:
  - name: api
    image: myorg/api:v1.2.3
    volumeMounts:
    - name: config-volume
      mountPath: /app/config     # Each key becomes a file at this path
      readOnly: true
    - name: secrets-volume
      mountPath: /run/secrets    # Secrets mounted here (tmpfs in practice)
      readOnly: true

  volumes:
  - name: config-volume
    configMap:
      name: app-config
  - name: secrets-volume
    secret:
      secretName: app-secrets
      defaultMode: 0400          # Owner read-only (most restrictive)

When you mount a ConfigMap at a path, each key becomes a file. The key nginx.conf becomes the file /app/config/nginx.conf. The key LOG_LEVEL becomes the file /app/config/LOG_LEVEL containing the text info.

Mount only specific keys

volumes:
- name: config-volume
  configMap:
    name: app-config
    items:
    - key: nginx.conf
      path: nginx.conf           # Mount only this key, at this filename
    - key: LOG_LEVEL
      path: log-level.txt

Encode Kubernetes Secret Values Instantly

Base64-encode your secret values before creating Kubernetes Secrets. Free, runs entirely in your browser - nothing leaves your machine.

Open Base64 Encoder/Decoder

Updating ConfigMaps and Secrets

This is where many teams get surprised. Environment variables injected via env or envFrom are not updated automatically. The Pod must be restarted to pick up new values. Files mounted from ConfigMaps are updated automatically (within ~60 seconds by default), but only if the application re-reads the file.

# Edit a ConfigMap directly
kubectl edit configmap app-config -n production

# Or apply an updated YAML
kubectl apply -f app-config.yaml

# Force Pods to pick up new env var values (rolling restart)
kubectl rollout restart deployment/my-api -n production

# Check the rollout
kubectl rollout status deployment/my-api -n production

Immutable ConfigMaps and Secrets

Setting immutable: true prevents accidental changes and improves cluster performance by disabling the watch on the object. Use this for configuration that should never change at runtime (static API endpoints, feature flag defaults):

apiVersion: v1
kind: ConfigMap
metadata:
  name: static-config
immutable: true
data:
  API_VERSION: "v2"
  REGION: "us-east-1"

External Secret Managers: The Production Standard

Kubernetes Secrets are only base64-encoded, not encrypted, by default. They can appear in etcd backups, cluster audit logs, and are accessible to anyone with the right RBAC permissions. For production, the recommended approach is to store secrets in a proper secret manager and sync them into Kubernetes using an operator:

  • External Secrets Operator (ESO) - pulls secrets from AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, and others. Creates and updates Kubernetes Secrets automatically.
  • Sealed Secrets - encrypts Secrets client side so they can be safely committed to Git. The cluster's controller decrypts them.
  • CSI Secret Store Driver - mounts secrets directly from external vaults as volume files, without creating a Kubernetes Secret at all.
# Example ExternalSecret with External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: app-secrets           # The Kubernetes Secret to create
    creationPolicy: Owner
  data:
  - secretKey: DATABASE_PASSWORD
    remoteRef:
      key: production/myapp    # AWS SM secret name
      property: db_password    # JSON key within the secret

Security Best Practices

  • Enable etcd encryption at rest for Secrets using Kubernetes' EncryptionConfiguration. Without this, Secrets in etcd are stored as base64 plaintext.
  • Apply least-privilege RBAC. Create a ServiceAccount per application and grant it only get on the specific Secrets it needs. Never use the default ServiceAccount for application Pods.
  • Never put Secrets in ConfigMaps. ConfigMaps have weaker access controls and are not treated as sensitive by Kubernetes.
  • Set defaultMode: 0400 on mounted Secret volumes so only the container user can read them.
  • Never commit Secret manifests with real values to Git. Use Sealed Secrets or ESO instead. Even base64-encoded values in a private repo are a liability.
  • Rotate secrets regularly. Update the Secret, then roll the Deployment. Automate rotation with your external secret manager.

Frequently Asked Questions

Are Kubernetes Secrets actually secure?

By default, Kubernetes Secrets are base64-encoded, not encrypted. They are as secure as your etcd access controls and RBAC policies. Anyone with kubectl get secret permission or direct etcd access can decode them trivially. Enable etcd encryption at rest and use an external secret manager (AWS Secrets Manager, Vault) for production workloads that handle sensitive credentials.

What is the size limit for a ConfigMap or Secret?

Both ConfigMaps and Secrets are limited to 1 MiB of data. This is enforced by the Kubernetes API server. If you need to inject large files (TLS bundles, large certificates, bulky config files), consider using a volume mount from a PersistentVolume or an init container that downloads from object storage.

Do I need to restart my Pods when I update a ConfigMap?

It depends on how the ConfigMap is consumed. If injected as environment variables (env or envFrom), yes - you must restart the Pod because environment variables are set at startup and never change. If mounted as a volume file, Kubernetes automatically updates the file in the container within about 60 seconds. But your application must also re-read the file for the change to take effect.

Can two Pods in different namespaces share the same ConfigMap?

No. ConfigMaps and Secrets are namespace-scoped. A Pod in namespace production cannot reference a ConfigMap in namespace staging. You must create identical ConfigMaps in each namespace, or use a tool like ESO that syncs Secrets across namespaces.

What is the difference between stringData and data in a Secret?

Both produce identical Secrets. stringData accepts plain text values and Kubernetes base64-encodes them automatically when storing. data requires values to be pre-encoded as base64. stringData is more convenient for humans and CI pipelines. You can mix both fields in one Secret - stringData values take precedence if the same key appears in both.

The Right Approach

For non-sensitive configuration, use ConfigMaps with envFrom for convenience or volume mounts for large files. For secrets, use an external secret manager synced with External Secrets Operator - this keeps plaintext values out of Git, out of your CI logs, and gives you rotation, auditing, and fine-grained access control. Use our free Base64 encoder when you need to manually encode Secret values.

Explore our 70+ free developer tools including the YAML Validator for validating your Kubernetes manifests.

Use our free tool here → Base64 Encoder/Decoder

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.