ConfigMap vs Secret #
When designing application architecture in Kubernetes, one of the most fundamental decisions we must make is determining where to store application configuration data. Kubernetes provides two built-in resources for separating configuration from application code (per the 12-Factor App principle): ConfigMap and Secret. Although both have similar API interfaces and can be consumed by Pods in nearly the same ways, they’re designed for very different security purposes, lifecycles, and storage mechanisms.
Choosing the wrong resource isn’t just a manifest code aesthetic issue — it can open serious security holes, complicate audit processes, and even disrupt system stability in production environments. This article presents an in-depth guide to help us choose precisely between ConfigMap and Secret through a battle-tested decision framework, low-level technical comparisons, real-world gray case analyses, decision flow diagrams, and a series of anti-patterns vs real production solutions.
The Configuration Separation Philosophy #
Before diving into technical comparisons, it’s important to understand why Kubernetes distinguishes these two objects. The basic philosophy is separation of concerns and the principle of least privilege.
ConfigMap is designed to represent what makes the application run, like configuration files, feature flags, ports, URLs, and performance tuning parameters. Data inside ConfigMaps is transparent and doesn’t need to be hidden from developers, monitoring systems, or general audit logs.
Conversely, Secret is designed to represent the application’s access keys or identity, like database passwords, third-party service API keys, TLS certificates, authentication tokens, and encryption keys. This data must be strictly protected, whether stored in the cluster database (etcd), transmitted over the network, or mounted into application containers.
The Decision Framework #
To make consistent choices across the entire engineering team, we can use a structured decision framework. This framework is based on data sensitivity levels, audit needs, and how the application consumes the data.
flowchart TD
Q["Is this data sensitive if exposed to the public or parties without full authority?"]
Q -->|YES| Secret["Use SECRET"]
Q -->|NO| ConfigMap["Use CONFIGMAP"]Here’s a quick checklist for grouping our application variables:
When Do We Use ConfigMap? #
- Application Settings: Internal application settings like
LOG_LEVEL(debug, info, warn, error),MAX_CONNECTION_POOL,REQUEST_TIMEOUT_SECONDS, orTHREAD_POOL_SIZE. - URLs and Hostnames: Internal or external endpoint addresses without authorization tokens, e.g.
DB_HOST=postgres-service.prod.svc.cluster.local,REDIS_PORT=6379, orPAYMENT_GATEWAY_URL=https://api.sandbox.midtrans.com. - Feature Flags: Dynamic configuration to enable or disable specific features without redeploying, like
ENABLE_NEW_DASHBOARD=trueorMAINTENANCE_MODE=false. - System Configuration Files: Whole text files read by web servers or proxies, like
nginx.conf,prometheus.yml,redis.conf, or XML/JSON logging configuration. - Environment Metadata: Environment identity markers, like
APP_ENV=productionorCLUSTER_REGION=ap-southeast-3.
When Do We Use Secret? #
- Authentication Credentials: Username and password combinations for databases, Redis, message brokers, or private repositories. Examples:
DB_PASSWORD,RABBITMQ_DEFAULT_PASS. - API Tokens and Private Keys: Access keys for sensitive external services. Examples:
STRIPE_SECRET_KEY,SENDGRID_API_KEY,AWS_SECRET_ACCESS_KEY. - Cryptographic Keys: Symmetric or asymmetric encryption keys used by the application to encrypt user data, sign JWTs (JSON Web Tokens), or do hashing. Examples:
JWT_SIGNING_KEY,AES_256_ENCRYPTION_KEY. - TLS/SSL Certificates: Public certificates and related private keys used to secure HTTPS or TLS communication on Ingress controllers or between pods (mTLS).
- Docker Registry Credentials: Authentication information (username, password, registry URL) in JSON format used by the Kubelet to pull container images from private registries (imagePullSecrets).
Low-Level Technical Comparison #
Although both look similar at the Kubernetes manifest level, how Kubernetes manages these two objects behind the scenes is very different. The table below summarizes the architectural and operational differences between ConfigMap and Secret:
| Comparison Aspect | ConfigMap | Secret |
|---|---|---|
| Data Representation in the API | Plaintext (immediately readable text) | Base64 Encoded (encoded to support binary data) |
| Storage in etcd | Plaintext by default | Plaintext by default (manual encryption at rest required) |
| Mount Media (Volume) | Stored on the node disk filesystem | Uses tmpfs (RAM-backed filesystem) to minimize disk traces |
| Maximum Size Limit | 1 MB per object | 1 MB per object |
| Access Control (RBAC) | Separate, but often granted broad read access | Very strict, usually limited to operators and service accounts |
| Audit Policy | Recorded in audit logs as regular data access | Recorded specially; payload contents can be hidden from activity logs |
| Special Object Types | None (Generic) | Has special sub-types (Opaque, TLS, ServiceAccountToken, etc.) |
| Rotation Mechanism | Auto-update (via symlink) within minutes | Auto-update (via symlink) within minutes |
Why Isn’t Base64 Encryption? #
There’s a common misunderstanding that data inside a Secret is safe because its value isn’t directly readable in the YAML manifest but appears as a random string like dGVzdHBhc3N3b3Jk. This string is actually just standard Base64 encoding meant to let Secrets hold binary data (like SSL certificates or small ZIP files) without breaking YAML parsers. Anyone with access to that Secret manifest can easily decode the string with a simple command:
echo "dGVzdHBhc3N3b3Jk" | base64 --decode
# Output: testpassword
Therefore, Secret security in Kubernetes doesn’t rely on Base64, but on strict RBAC (Role-Based Access Control) restrictions and KMS (Key Management Service) configuration to encrypt the data before writing it to the etcd database.
The Decision Tree #
To ensure standardization of resource selection, we can refer to the flow diagram below when reviewing application configuration:
flowchart TD
Start["Identify the Variable / Config File"] --> C1{"Is the data sensitive?<br>'(Password, API Key, Cert, Token)'"}
C1 -- "Yes (Sensitive)" --> SecretObj["CHOOSE: SECRET"]
C1 -- "No (Non-Sensitive)" --> C2{"Does the data differ<br>between environments?"}
C2 -- "Yes (Dynamic)" --> C3{"Is the data size<br>larger than 1 MB?"}
C2 -- "No (Static)" --> Hardcode["Hardcode in Code/Dockerfile<br>or use default values"]
C3 -- "Yes" --> ExternalStorage["Use External Storage<br/>'(Persistent Volume, S3, etc.)'"]
C3 -- "No" --> ConfigMapObj["CHOOSE: CONFIGMAP"]
style SecretObj stroke:#d32f2f,stroke-width:2px
style ConfigMapObj stroke:#388e3c,stroke-width:2px
style ExternalStorage stroke:#f57c00,stroke-width:2pxReal-World Gray Cases #
In field practice, we often find variables sitting in gray areas. These parameters aren’t exactly state secrets, but they also aren’t appropriate for free sharing. Here’s how we analyze and decide their placement:
1. Full Database Connection Strings #
A connection string usually looks like this: postgresql://app_user:[email protected]:5432/main_db.
- Analysis: Although it contains non-sensitive host names and ports, this string also explicitly includes a username and password.
- Decision: Must go into SECRET. Splitting this string into non-sensitive (ConfigMap) and sensitive (Secret) parts, then recombining them at the application code level, is a highly recommended best practice.
2. Internal Service URLs with Authentication Tokens #
For example, a webhook endpoint: https://slack.com/services/hooks/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX.
- Analysis: The Slack domain is public, but the webhook token at the end of the URL grants direct access to send messages to our company’s internal channels.
- Decision: Must go into SECRET. Exposing the Slack webhook URL to logs or Git repositories can lead to spam exploitation or internal information leaks.
3. Certificate Authority (CA) Certificates #
The Root CA certificate (ca.crt) is used to verify external or internal server identities.
- Analysis: Unlike a private key, a public certificate can’t be used to impersonate a server. This certificate is widely distributed to build a trust chain.
- Decision: Use CONFIGMAP. However, if the certificate is bundled together with a private key in one TLS handshake unit (like on web server certificates), the entire bundle (public + private) must be stored in SECRET.
4. Admin Usernames (Without Passwords) #
Configurations like ADMIN_USERNAME=admin or DB_USER=root.
- Analysis: A username isn’t the main authentication secret, but this information narrows the search space for attackers wanting to do brute-force attacks.
- Decision: On clusters with standard security levels, this data is fine in CONFIGMAP. However, in high-compliance environments (like PCI-DSS or HIPAA), putting usernames into SECRET alongside their passwords is the operational standard.
Anti-Patterns vs Best Solutions #
Let’s study common mistakes that often happen in the real world and how we can fix them with safe, efficient solutions.
Anti-Pattern 1: Mixing Sensitive Credentials into ConfigMaps #
The act of storing passwords or tokens directly in ConfigMap data under the guise of practicality. This cancels all the RBAC security policies we usually apply to Secrets.
# ✗ ANTI-PATTERN: Storing database passwords inside a ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-production
namespace: payment
data:
DB_HOST: "mysql-db-prod.internal"
DB_PORT: "3306"
DB_NAME: "payment_db"
DB_USER: "pay_admin"
DB_PASSWORD: "SuperSecretPassword123" # ← CRITICAL: Credentials leak to anyone with ConfigMap read access
Best Solution #
Separate non-sensitive variables into a ConfigMap, and store sensitive variables in a Secret. Combine both when defining the Pod using envFrom or individual variable references.
# ✓ SOLUTION: Separate the non-sensitive data
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-production
namespace: payment
data:
DB_HOST: "mysql-db-prod.internal"
DB_PORT: "3306"
DB_NAME: "payment_db"
DB_USER: "pay_admin"
---
# ✓ SOLUTION: Store credentials in an Opaque Secret
apiVersion: v1
kind: Secret
metadata:
name: app-credentials-production
namespace: payment
type: Opaque
stringData:
DB_PASSWORD: "SuperSecretPassword123" # ← Safe, managed by the Secret API with etcd encryption
Anti-Pattern 2: Putting an Entire .env File into One ConfigMap
#
Many developer teams move their local .env files raw into one ConfigMap entry. That .env file usually contains a mix of framework configuration, URLs, and production API keys.
# ✗ ANTI-PATTERN: Wrapping the entire .env file (mixed data) into a ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: monolith-dotenv
namespace: core
data:
.env: |
NODE_ENV=production
PORT=8080
DEBUG=false
DATABASE_URL=postgresql://monolith:change-me@db-host:5432/core
STRIPE_KEY=sk_live_EXAMPLEDUMMYKEY123456 # ← CRITICAL: Live Stripe token exposed in a ConfigMap
Best Solution #
We must split the .env file into two separate files before deploying to Kubernetes, or use the individual environment variable mapping mechanism at the Deployment manifest level.
# ✓ SOLUTION: Define a ConfigMap for non-sensitive environment variables
apiVersion: v1
kind: ConfigMap
metadata:
name: monolith-config
namespace: core
data:
NODE_ENV: "production"
PORT: "8080"
DEBUG: "false"
DATABASE_USER: "monolith"
DATABASE_HOST: "db-host"
DATABASE_PORT: "5432"
DATABASE_NAME: "core"
---
# ✓ SOLUTION: Define a Secret for sensitive data
apiVersion: v1
kind: Secret
metadata:
name: monolith-secret
namespace: core
type: Opaque
stringData:
DATABASE_PASSWORD: "SuperPassword"
STRIPE_KEY: "sk_live_EXAMPLEDUMMYKEY123456"
Our application can then seamlessly consume this configuration through a Deployment by reassembling the needed variables:
# ✓ SOLUTION: Consume both resources at the Deployment level
apiVersion: apps/v1
kind: Deployment
metadata:
name: monolith-app
namespace: core
spec:
replicas: 3
selector:
matchLabels:
app: monolith
template:
metadata:
labels:
app: monolith
spec:
containers:
- name: app
image: company/monolith:v1.2.0
envFrom:
- configMapRef:
name: monolith-config
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: monolith-secret
key: DATABASE_PASSWORD
- name: DATABASE_URL
value: "postgresql://$(DATABASE_USER):$(DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(DATABASE_NAME)"
- name: STRIPE_KEY
valueFrom:
secretKeyRef:
name: monolith-secret
key: STRIPE_KEY
Anti-Pattern 3: Hardcoding Secrets Inside Docker Images (Build-Time Secrets) #
The act of writing passwords or API keys directly in a Dockerfile using the ENV instruction or copying a local .env file into the image during the build process.
# ✗ ANTI-PATTERN: Storing permanent credentials inside an image layer
FROM node:18-alpine
ENV DATABASE_URL="postgresql://user:change-me@host:5432/db"
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "server.js"]
Best Solution #
Make our container images stateless and environment-agnostic. Read all dynamic configuration and secrets from environment variables injected by Kubernetes at runtime (not build-time).
# ✓ SOLUTION: Clean Dockerfile without storing sensitive configuration
FROM node:18-alpine
COPY . /app
WORKDIR /app
RUN npm install --only=production
# Make sure the application code reads process.env.DATABASE_URL
CMD ["node", "server.js"]
Anti-Pattern 4: Not Enabling Encryption at Rest for Secrets in etcd #
By default, data in the Kubernetes etcd database is stored as plaintext. If someone breaks into the etcd machine or steals etcd backup snapshots, all cluster Secrets get exposed immediately.
flowchart LR
APIServer["kube-apiserver"] -->|"Plaintext Secret"| ETCD["etcd (DB_PASSWORD)"]Best Solution #
Enable encryption on the etcd database using an EncryptionConfiguration object with encryption providers like AES-GCM or cloud KMS (Key Management Service) (AWS KMS, GCP Cloud KMS, Azure Key Vault).
# ✓ SOLUTION: EncryptionConfiguration config file for the API Server
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms: # Uses external KMS for high-level security
name: aws-kms-provider
endpoint: unix:///var/run/kmsprovider/kms.sock
cachesize: 1000
timeout: 3s
- aescbc: # Fallback using a local AES-CBC key
keys:
- name: key1
secret: c3VwZXJzZWNyZXRrZXk= # Base64 of a 32-byte key
Mounting Mechanisms #
We can consume ConfigMap and Secret into Pods using two main methods: Environment Variables or Volume Mounts (Files on the Filesystem). Each method has significant technical implications for security and dynamic data updates.
1. Using Environment Variables #
Data is directly mapped into container environment variables.
- Advantages: Very easy to read from almost all application frameworks (Node.js
process.env, Pythonos.environ). - Disadvantages:
- Can’t be dynamically updated (Static). If ConfigMap or Secret values in the cluster change, we must restart (rolling restart) the Pod so the application reads new values.
- Information Leakage Risk. Environment variables often get recorded in crash dumps, container debug logs, or exposed when running debugging commands like
kubectl describe podorenvinside the container.
2. Using Volume Mounts #
Data is mapped as files in a dedicated container directory (e.g. /etc/config or /run/secrets).
- Advantages:
- Supports Dynamic Updates (Hot Reload / Symlink Swap). The Kubelet periodically checks for ConfigMap/Secret changes. When changes occur in the cluster, the Kubelet automatically updates files inside the container using an atomic symlink swap mechanism without restarting the Pod.
- Better Security (Secret-specific). Secrets mounted as volumes use the
tmpfs(RAM) filesystem. Data is never written to the worker node’s physical disk, so no forensic traces remain if the node’s storage media gets seized or hacked.
- Disadvantages: Applications must be specifically programmed to listen for file changes (using watcher libraries like
fsnotifyor detecting inotify change events) to reload configuration directly in memory.
sequenceDiagram
participant Dev as Developer
participant API as Kubernetes API
participant Kube as Kubelet Daemon
participant Pod as Pod Container (Volume Mount)
Dev->>API: Change the ConfigMap value in the Cluster
API-->>Kube: Sync the object (Watch Event)
Kube->>Pod: Update files via atomic symlink swap
Note over Pod: Files updated without restart!Pod Configuration Architecture Review Checklist #
Before launching applications to production, make sure our engineering team audits configuration using the following checklist:
SENSITIVE DATA MANAGEMENT:
□ All credentials (API keys, passwords, tokens) have been moved from ConfigMaps to Secrets.
□ No sensitive configuration is listed in Dockerfiles (ENV) or hardcoded in source code.
□ Database connection strings are split into hostname (ConfigMap) and credentials (Secret).
ACCESS CONTROL & INFRASTRUCTURE SECURITY:
□ RBAC access to the Secret API is strictly limited to the application service accounts that need it.
□ Encryption at rest for Secrets in etcd has been enabled at the cluster control plane level.
□ Application containers run as non-root users to limit access to mounted Secret files.
OPERATIONS & MAINTENANCE:
□ Total ConfigMap or Secret size doesn't approach the etcd maximum limit (1 MB).
□ For frequently changing dynamic configuration, the consumption method (Volume vs Env) is confirmed to support the application reload lifecycle.
□ Large configuration files (like Nginx proxy configs) are mounted as Volumes to avoid etcd size limitations.
Summary #
- Distinguish the fundamental roles — ConfigMap is used for non-sensitive application operational settings; Secret is used to protect credentials, certificates, and authentication tokens.
- Base64 isn’t encryption — Base64-encoded Secret values only exist for binary data compatibility; real protection comes from RBAC restrictions and enabling KMS Encryption at rest in etcd.
- Use Volume Mounts for dynamic updates — Mounting ConfigMap/Secret as volumes enables direct data updates (hot reload) inside containers without Pod restarts, unlike static Environment Variables.
- Split connection strings — Avoid writing full URIs containing credentials inside ConfigMaps; split the host into a ConfigMap and the password into a Secret, then combine at the Pod manifest or application code level.
- Respect the 1 MB size limit — Both ConfigMap and Secret have a hard size limit of 1 MB per object due to etcd architectural constraints; use external volumes or object storage for giant config files.
- Use tmpfs for Secrets — Mounting Secrets as volumes leverages the worker node host’s RAM filesystem (
tmpfs) to guarantee credentials are never written to physical storage media.
← Previous: External Secret Manager Next: Configuration Hot Reload →