Introduction
Hardcoding configuration values and sensitive data directly into your application code is a recipe for disaster. It makes your applications inflexible, insecure, and difficult to manage across different environments. Kubernetes provides two powerful objects to solve this problem: ConfigMaps for configuration data and Secrets for sensitive information.
In this comprehensive guide, I’ll show you how to effectively manage application configuration and secrets in Kubernetes, following security best practices and production patterns.
Why Separate Configuration from Code?
The Problem:
# Bad Practice - Hardcoded values
containers:
- name: app
image: myapp:1.0
env:
- name: DATABASE_URL
value: "postgres://admin:password123@db.prod.com:5432/mydb" # Exposed!
- name: API_KEY
value: "sk-1234567890abcdef" # Security risk!
The Solution:
- ConfigMaps: Non-sensitive configuration (URLs, feature flags, settings)
- Secrets: Sensitive data (passwords, API keys, certificates)
Benefits:
- Security: Secrets are base64 encoded and can be encrypted at rest
- Flexibility: Change configuration without rebuilding images
- Environment separation: Different configs for dev/staging/prod
- Reusability: Share configuration across multiple Pods
- Version control: Track configuration changes
Prerequisites
- kubectl installed and configured
- Access to a Kubernetes cluster
- Understanding of Pods and Deployments
- Basic knowledge of environment variables
Part 1: ConfigMaps
ConfigMaps store non-confidential configuration data as key-value pairs.
Creating ConfigMaps
Method 1: Declarative (YAML)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
# Simple key-value pairs
database_url: "db.example.com"
database_name: "myapp"
log_level: "info"
# Multi-line configuration
app.properties: |
color=blue
padding=25px
theme=dark
# JSON configuration
config.json: |
{
"api": {
"timeout": 30,
"retries": 3
}
}
Create:
kubectl apply -f configmap.yaml
Method 2: Imperative (Command Line)
From literal values:
kubectl create configmap app-config \
--from-literal=database_url=db.example.com \
--from-literal=database_name=myapp \
--from-literal=log_level=info
From files:
# Create config file
echo "color=blue" > app.properties
echo "theme=dark" >> app.properties
kubectl create configmap app-config --from-file=app.properties
From directory:
kubectl create configmap app-config --from-file=./config-dir/
From environment file:
# Create .env file
cat <<EOF > app.env
DATABASE_URL=db.example.com
LOG_LEVEL=info
CACHE_ENABLED=true
EOF
kubectl create configmap app-config --from-env-file=app.env
Using ConfigMaps in Pods
Method 1: Environment Variables (Individual Keys)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
env:
- name: DATABASE_URL
valueFrom:
configMapKeyRef:
name: app-config
key: database_url
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: log_level
Method 2: Environment Variables (All Keys)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
envFrom:
- configMapRef:
name: app-config
Method 3: Volume Mounts
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
Access files in the Pod:
kubectl exec -it app-pod -- sh
ls /etc/config/
cat /etc/config/database_url
cat /etc/config/app.properties
Method 4: Specific Keys as Files
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: app.properties
path: application.properties
- key: config.json
path: config.json
ConfigMap Management
View ConfigMaps:
kubectl get configmaps
kubectl get cm # Short form
kubectl describe configmap app-config
kubectl get configmap app-config -o yaml
Edit ConfigMap:
kubectl edit configmap app-config
Delete ConfigMap:
kubectl delete configmap app-config
Important: Pods don’t automatically reload when ConfigMaps change. You need to:
- Restart Pods manually
- Use a sidecar container to watch for changes
- Implement application-level config reloading
Part 2: Secrets
Secrets store sensitive information like passwords, tokens, and keys.
Secret Types
Kubernetes supports several Secret types:
- Opaque (default): Arbitrary user-defined data
- kubernetes.io/service-account-token: Service account token
- kubernetes.io/dockercfg: Docker registry credentials
- kubernetes.io/tls: TLS certificate and key
- kubernetes.io/ssh-auth: SSH authentication
- kubernetes.io/basic-auth: Basic authentication
Creating Secrets
Method 1: Declarative (YAML)
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData: # Use stringData for plain text (auto base64 encoded)
database_password: "MyS3cr3tP@ssw0rd"
api_key: "sk-1234567890abcdef"
# Or use data with base64 encoded values
data:
username: YWRtaW4= # base64 encoded "admin"
Create:
kubectl apply -f secret.yaml
Method 2: Imperative (Command Line)
From literal values:
kubectl create secret generic app-secrets \
--from-literal=database_password='MyS3cr3tP@ssw0rd' \
--from-literal=api_key='sk-1234567890abcdef'
From files (recommended for security):
# Create secret files
echo -n 'MyS3cr3tP@ssw0rd' > password.txt
echo -n 'sk-1234567890abcdef' > api-key.txt
kubectl create secret generic app-secrets \
--from-file=database_password=password.txt \
--from-file=api_key=api-key.txt
# Clean up files
rm password.txt api-key.txt
From JSON file:
cat <<EOF > credentials.json
{
"apiKey": "sk-1234567890abcdef",
"clientSecret": "secret123"
}
EOF
kubectl create secret generic app-secrets --from-file=credentials.json
rm credentials.json
TLS Secret:
kubectl create secret tls tls-secret \
--cert=path/to/cert.crt \
--key=path/to/key.key
Docker Registry Secret:
kubectl create secret docker-registry regcred \
--docker-server=docker.io \
--docker-username=myuser \
--docker-password=mypassword \
--docker-email=myemail@example.com
Using Secrets in Pods
Method 1: Environment Variables (Individual Keys)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: database_password
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: api_key
Method 2: Environment Variables (All Keys)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
envFrom:
- secretRef:
name: app-secrets
Method 3: Volume Mounts (Recommended)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: app-secrets
Access secrets in the Pod:
kubectl exec -it app-pod -- sh
ls /etc/secrets/
cat /etc/secrets/database_password
cat /etc/secrets/api_key
Method 4: Specific Keys with Custom Paths
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:alpine
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
volumes:
- name: secret-volume
secret:
secretName: app-secrets
items:
- key: database_password
path: db-password
mode: 0400 # Read-only for owner
Using Docker Registry Secrets
apiVersion: v1
kind: Pod
metadata:
name: private-app
spec:
containers:
- name: app
image: myregistry.com/myapp:1.0
imagePullSecrets:
- name: regcred
Secret Management
View Secrets:
kubectl get secrets
kubectl describe secret app-secrets
kubectl get secret app-secrets -o yaml
Decode Secret values:
kubectl get secret app-secrets -o jsonpath='{.data.database_password}' | base64 --decode
Edit Secret:
kubectl edit secret app-secrets
Delete Secret:
kubectl delete secret app-secrets
Complete Example: Multi-Tier Application
Let’s create a complete application with both ConfigMaps and Secrets:
configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: web-app-config
namespace: production
data:
# Application settings
app_name: "MyWebApp"
environment: "production"
log_level: "info"
cache_ttl: "3600"
# Feature flags
feature_flags: |
new_ui=true
beta_features=false
analytics=true
# Nginx configuration
nginx.conf: |
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend:8080;
}
}
secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: web-app-secrets
namespace: production
type: Opaque
stringData:
database_url: "postgres://db.prod.example.com:5432/myapp"
database_user: "app_user"
database_password: "Str0ngP@ssw0rd!"
jwt_secret: "your-256-bit-secret"
api_key: "sk-prod-1234567890abcdef"
deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
# Environment variables from ConfigMap
env:
- name: APP_NAME
valueFrom:
configMapKeyRef:
name: web-app-config
key: app_name
- name: ENVIRONMENT
valueFrom:
configMapKeyRef:
name: web-app-config
key: environment
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: web-app-config
key: log_level
# Environment variables from Secret
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: web-app-secrets
key: database_url
- name: DATABASE_USER
valueFrom:
secretKeyRef:
name: web-app-secrets
key: database_user
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: web-app-secrets
key: database_password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: web-app-secrets
key: jwt_secret
# Mount ConfigMap as volume
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: config-volume
configMap:
name: web-app-config
- name: secret-volume
secret:
secretName: web-app-secrets
defaultMode: 0400
Deploy:
kubectl create namespace production
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
kubectl apply -f deployment.yaml
Security Best Practices
1. Never Commit Secrets to Git
# Add to .gitignore
echo "*.secret.yaml" >> .gitignore
echo "secrets/" >> .gitignore
2. Use RBAC to Restrict Access
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
resourceNames: ["app-secrets"] # Specific secret only
3. Enable Encryption at Rest
# EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-secret>
- identity: {}
4. Use External Secret Management
- AWS Secrets Manager
- Azure Key Vault
- HashiCorp Vault
- Google Secret Manager
Example with External Secrets Operator:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-secrets
data:
- secretKey: database_password
remoteRef:
key: prod/database/password
5. Rotate Secrets Regularly
# Update secret
kubectl create secret generic app-secrets \
--from-literal=api_key='new-key' \
--dry-run=client -o yaml | kubectl apply -f -
# Restart pods to pick up new secret
kubectl rollout restart deployment web-app
6. Use Immutable ConfigMaps/Secrets
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
immutable: true # Cannot be updated
data:
key: value
7. Limit Secret Scope
# Use namespaces to isolate secrets
kubectl create namespace dev
kubectl create namespace prod
# Secrets in dev namespace can't be accessed from prod
Troubleshooting
Issue 1: Pod Can’t Access ConfigMap/Secret
Check if exists:
kubectl get configmap app-config
kubectl get secret app-secrets
Check namespace:
kubectl get configmap app-config -n production
Verify Pod reference:
kubectl describe pod app-pod
# Look for events related to ConfigMap/Secret mounting
Issue 2: Secret Values Not Decoded
Decode manually:
kubectl get secret app-secrets -o jsonpath='{.data.password}' | base64 --decode
Issue 3: ConfigMap Changes Not Reflected
Restart Pods:
kubectl rollout restart deployment web-app
Or use annotation to force update:
kubectl patch deployment web-app \
-p '{"spec":{"template":{"metadata":{"annotations":{"configmap-version":"'$(date +%s)'"}}}}}'
ConfigMap vs Secret Comparison
Feature | ConfigMap | Secret |
---|---|---|
Purpose | Non-sensitive config | Sensitive data |
Encoding | Plain text | Base64 encoded |
Encryption | No | Optional (at rest) |
Size Limit | 1MB | 1MB |
Use Cases | URLs, settings, flags | Passwords, keys, certs |
Visibility | Visible in YAML | Hidden in kubectl describe |
Advanced Patterns
1. ConfigMap/Secret Versioning
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2 # Version in name
data:
version: "2.0"
setting: "value"
2. Multiple Environments
# Different configs per environment
kubectl create configmap app-config --from-file=config.dev.yaml -n dev
kubectl create configmap app-config --from-file=config.prod.yaml -n prod
3. Shared Secrets Across Namespaces
# Copy secret to another namespace
kubectl get secret app-secrets -n source -o yaml | \
sed 's/namespace: source/namespace: target/' | \
kubectl apply -f -
Conclusion
Proper configuration management is crucial for secure, maintainable Kubernetes applications. Key takeaways:
- Use ConfigMaps for non-sensitive configuration
- Use Secrets for sensitive data
- Never hardcode credentials in images
- Implement RBAC to restrict access
- Enable encryption at rest for Secrets
- Rotate secrets regularly
- Consider external secret management solutions
- Use volumes over environment variables for sensitive data
- Implement proper namespace isolation
Mastering ConfigMaps and Secrets enables you to build secure, flexible applications that can easily adapt to different environments without code changes.
In the next post, we’ll explore Kubernetes Storage with Persistent Volumes and Persistent Volume Claims.
Happy configuring! 🔐