---
title: "ConfigMaps and Secrets: Configuration Management in Kubernetes"
description: "How to create, mount, and manage ConfigMaps and Secrets, including propagation behavior, immutability, Secret types, and the base64 encoding gotcha."
url: https://agent-zone.ai/knowledge/kubernetes/configmaps-and-secrets/
section: knowledge
date: 2026-02-22
categories: ["kubernetes"]
tags: ["configmaps","secrets","configuration","volumes","environment-variables"]
skills: ["configuration-management","secret-handling","pod-configuration"]
tools: ["kubectl","helm"]
levels: ["intermediate"]
word_count: 743
formats:
  json: https://agent-zone.ai/knowledge/kubernetes/configmaps-and-secrets/index.json
  html: https://agent-zone.ai/knowledge/kubernetes/configmaps-and-secrets/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=ConfigMaps+and+Secrets%3A+Configuration+Management+in+Kubernetes
---


# ConfigMaps and Secrets

ConfigMaps hold non-sensitive configuration data. Secrets hold sensitive data like passwords, tokens, and TLS certificates. They look similar in structure but differ in handling: Secrets are base64-encoded, stored with slightly restricted access by default, and can be encrypted at rest if the cluster is configured for it.

## Creating ConfigMaps

**From a literal value:**

```bash
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=MAX_CONNECTIONS=100
```

**From a file:**

```bash
kubectl create configmap nginx-config --from-file=nginx.conf
```

The key name defaults to the filename. Override it with `--from-file=custom-key=nginx.conf`.

**From an env file:**

```bash
# app.env contains KEY=VALUE pairs, one per line
kubectl create configmap app-config --from-env-file=app.env
```

**Declarative YAML (recommended for version control):**

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: payments-prod
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  config.yaml: |
    server:
      port: 8080
      timeout: 30s
    database:
      pool_size: 10
```

## Creating Secrets

**From literals:**

```bash
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='s3cret!@#'
```

kubectl automatically base64-encodes the values.

**Declarative YAML:**

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: payments-prod
type: Opaque
data:
  username: YWRtaW4=        # echo -n "admin" | base64
  password: czNjcmV0IUAj    # echo -n "s3cret!@#" | base64
```

### The Base64 Gotcha

Values under `data:` must be base64-encoded. If you put plaintext there, Kubernetes accepts it silently but your application receives garbage after double-decoding. Use `stringData:` to avoid this entirely:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  username: admin
  password: "s3cret!@#"
```

`stringData` is write-only. When you `kubectl get secret -o yaml`, values always appear base64-encoded under `data`. But `stringData` is the safer way to define secrets in manifests because you write plaintext and Kubernetes encodes it for you.

## Secret Types

Kubernetes defines several built-in Secret types:

- **`Opaque`** -- Generic key-value pairs. The default.
- **`kubernetes.io/dockerconfigjson`** -- Image pull credentials for private registries.
- **`kubernetes.io/tls`** -- TLS certificate and private key.
- **`kubernetes.io/basic-auth`** -- Username and password (keys: `username`, `password`).
- **`kubernetes.io/service-account-token`** -- Automatically created for ServiceAccounts (legacy, pre-1.24).

Creating a TLS secret:

```bash
kubectl create secret tls my-tls-cert \
  --cert=path/to/cert.pem \
  --key=path/to/key.pem
```

Creating an image pull secret:

```bash
kubectl create secret docker-registry registry-cred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass
```

Then reference it in your pod spec:

```yaml
spec:
  imagePullSecrets:
  - name: registry-cred
```

## Mounting as Volumes vs Environment Variables

### Environment Variables

```yaml
spec:
  containers:
  - name: app
    env:
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: LOG_LEVEL
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password
```

Or inject all keys at once with `envFrom`:

```yaml
spec:
  containers:
  - name: app
    envFrom:
    - configMapRef:
        name: app-config
    - secretRef:
        name: db-credentials
```

### Volume Mounts

```yaml
spec:
  containers:
  - name: app
    volumeMounts:
    - name: config-volume
      mountPath: /etc/app/config
      readOnly: true
    - name: secret-volume
      mountPath: /etc/app/secrets
      readOnly: true
  volumes:
  - name: config-volume
    configMap:
      name: app-config
  - name: secret-volume
    secret:
      secretName: db-credentials
      defaultMode: 0400
```

Each key becomes a file in the mount directory. The file contents are the values. For Secrets mounted as volumes, set `defaultMode: 0400` to restrict file permissions.

To mount a single key as a specific file (not a directory):

```yaml
volumes:
- name: config-volume
  configMap:
    name: app-config
    items:
    - key: config.yaml
      path: config.yaml
```

## When Changes Propagate to Pods

This is the single most important behavioral difference to understand:

**Environment variables from ConfigMaps/Secrets do NOT update when the source changes.** The values are injected at pod creation time. To pick up changes, you must restart the pod. There is no way around this.

**Volume-mounted ConfigMaps/Secrets DO update automatically**, but with a delay. The kubelet syncs mounted ConfigMaps roughly every 60-120 seconds (configurable via `--sync-frequency`). Your application must watch the mounted files for changes or periodically re-read them.

A common pattern to force pod restarts on ConfigMap changes in Helm:

```yaml
# In your Deployment template
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
```

This changes the pod template annotation whenever the ConfigMap content changes, triggering a rolling update.

## Immutable ConfigMaps and Secrets

Marking a ConfigMap or Secret as immutable prevents accidental changes and improves cluster performance (the kubelet stops polling for updates):

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v2
immutable: true
data:
  LOG_LEVEL: "warn"
```

Once set, `immutable` cannot be changed back to `false`. You must delete and recreate the resource. This encourages a versioning pattern: `app-config-v1`, `app-config-v2`, with deployments referencing specific versions.

## Key Takeaways

- Use `stringData` in Secret manifests to avoid base64 encoding mistakes.
- Environment variables are set at pod creation and never update. Volume mounts update with a delay.
- Always set `defaultMode: 0400` on Secret volume mounts.
- Use the Helm `sha256sum` annotation trick to trigger rolling updates on config changes.
- Immutable ConfigMaps reduce API server load and prevent accidental changes; version them explicitly.
- Choose the correct Secret type (`tls`, `dockerconfigjson`, etc.) rather than stuffing everything into `Opaque`.

