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
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
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.
| Type | Use Case | Required Keys |
|---|---|---|
Opaque | Arbitrary user-defined data (passwords, API keys) | Any |
kubernetes.io/tls | TLS certificate + private key | tls.crt, tls.key |
kubernetes.io/dockerconfigjson | Docker registry credentials (pull secrets) | .dockerconfigjson |
kubernetes.io/service-account-token | Service account tokens (auto-created) | token, ca.crt |
kubernetes.io/basic-auth | HTTP basic auth credentials | username, password |
kubernetes.io/ssh-auth | SSH private key | ssh-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!
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
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.