Secrets & ConfigMaps
Externalising configuration and protecting sensitive data in Helm charts
Why Externalise Configuration?
The twelve-factor app methodology tells us to store configuration in the environment, not in the code. In Kubernetes this translates to two native resources: ConfigMaps for non-sensitive data and Secrets for credentials and keys. Helm adds a templating layer that makes both resources dynamic, reusable, and environment-aware. Understanding how to create, mount, and secure these resources is essential for any production-grade chart.
ConfigMaps
Creating ConfigMaps from Values
The most common pattern is to template ConfigMap entries directly from values.yaml. Each key-value pair becomes a data entry the pod can consume as an environment variable or a file.
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
labels:
{{- include "myapp.labels" . | nindent 4 }}
data:
APP_ENV: {{ .Values.config.env | quote }}
LOG_LEVEL: {{ .Values.config.logLevel | quote }}
MAX_CONNECTIONS: {{ .Values.config.maxConnections | quote }}
Always use | quote on values that originate from values.yaml. Without it, YAML might interpret strings like "true" or "1.0" as booleans or floats, causing subtle bugs.
Loading Files with .Files.Get and .Files.Glob
Helm can embed entire configuration files stored inside the chart directory. Place files in a files/ directory at the chart root and reference them in the template.
data:
nginx.conf: |-
{{ .Files.Get "files/nginx.conf" | indent 4 }}
app.properties: |-
{{ .Files.Get "files/app.properties" | indent 4 }}
For loading multiple files at once, .Files.Glob returns a map you can iterate over:
data:
{{- range $path, $bytes := .Files.Glob "files/**.conf" }}
{{ base $path }}: |-
{{ $.Files.Get $path | indent 4 }}
{{- end }}
Note
Helm excludes the templates/ directory and Chart.yaml from .Files. Any other file at the chart root (or in subdirectories) is accessible.
Mounting ConfigMaps
Kubernetes offers two ways to consume ConfigMap data:
| Method | How | Live Updates? |
|---|---|---|
| Environment variables | envFrom or individual configMapKeyRef | No — requires pod restart |
| Volume mount | volumes + volumeMounts | Yes — kubelet syncs ~60s |
Volume mounts are preferred when the application can watch for file changes (e.g., nginx reload, Spring Cloud Config). Environment variables are simpler for applications that only read config at startup.
Secrets in Helm
Secret Types
Kubernetes recognises several Secret types. The three most common in Helm charts are:
- Opaque — General-purpose; the default type for arbitrary key-value pairs like database passwords and API tokens.
- kubernetes.io/tls — Stores
tls.crtandtls.key. Used by Ingress controllers for HTTPS termination. - kubernetes.io/dockerconfigjson — Stores registry authentication credentials for pulling private images.
data vs stringData
The data field expects base64-encoded values. Helm provides the b64enc function for this:
apiVersion: v1
kind: Secret
metadata:
name: {{ include "myapp.fullname" . }}-secret
type: Opaque
data:
DB_PASSWORD: {{ .Values.secrets.dbPassword | b64enc | quote }}
Alternatively, stringData accepts plaintext strings. Kubernetes base64-encodes them on creation:
stringData:
API_KEY: {{ .Values.secrets.apiKey | quote }}
Both approaches produce the same result at rest. The stringData field is more readable in templates but be aware that the plaintext will be visible in the rendered manifest (e.g., helm template output).
The Plaintext Problem
Critical
Storing secrets in values.yaml means plaintext credentials in your Git repository. Even in a private repo, this violates the principle of least privilege and makes credential rotation painful.
This is the most common security mistake in Helm charts. The values file is meant to be committed to version control, yet teams routinely place database passwords, API keys, and TLS private keys directly in it. Several solutions exist, each with different trade-offs.
External Secrets Operator
The External Secrets Operator (ESO) introduces an ExternalSecret CRD that syncs secrets from an external vault into a native Kubernetes Secret. Your Helm chart templates an ExternalSecret instead of a Secret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "myapp.fullname" . }}-es
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: {{ include "myapp.fullname" . }}-secret
creationPolicy: Owner
data:
- secretKey: DB_PASSWORD
remoteRef:
key: prod/myapp/db
property: password
ESO supports AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager, and many more backends. The chart only contains a reference — the actual secret value never appears in Git.
Sealed Secrets
Bitnami Sealed Secrets takes a different approach. You encrypt secrets locally using the kubeseal CLI and commit the encrypted SealedSecret resource. Only the Sealed Secrets controller in the target cluster can decrypt it. This is useful when you want everything in Git (true GitOps) without exposing plaintext.
Helm Secrets Plugin
The helm-secrets plugin integrates Mozilla SOPS or age encryption into the Helm workflow. You encrypt your values files before committing and decrypt them transparently during helm install or helm upgrade. It adds minimal friction to existing workflows.
Immutable ConfigMaps and Secrets
Kubernetes 1.21+ supports the immutable: true field on both ConfigMaps and Secrets. Once set, the resource cannot be modified — only deleted and recreated.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
immutable: true
data:
APP_ENV: {{ .Values.config.env | quote }}
Benefits of immutable resources:
- Performance — The API server stops watching for changes, reducing load significantly in clusters with thousands of ConfigMaps.
- Safety — Prevents accidental modification of critical configuration.
- Predictability — Guarantees that running pods see the exact configuration they started with.
The trade-off is that updates require a delete-and-recreate cycle. Helm handles this gracefully during helm upgrade because it computes a three-way merge and replaces resources whose spec changed.
The Checksum Annotation Trick
By default, changing a ConfigMap or Secret does not trigger a pod rollout. Pods keep running with stale configuration. The standard Helm solution is to add a checksum annotation to the pod template:
# templates/deployment.yaml
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
When the ConfigMap or Secret template changes, the SHA-256 hash changes, which modifies the pod template spec, which triggers a rolling update. This is an elegant, zero-dependency solution that works with any Kubernetes version.
Tip
If you are using External Secrets, the checksum trick does not apply (the Helm template does not change when the remote secret rotates). Use the Stakater Reloader controller instead, which watches for changes on the actual Secret object.
Secret Rotation Patterns
Credentials should be rotated regularly. How you implement rotation depends on your secrets management approach:
- External Secrets Operator — Set
refreshInterval(e.g.,15m) so the operator polls the vault. Combine with Reloader for automatic pod restarts. - Dual-credential rotation — Keep both old and new credentials valid simultaneously. Roll out the new credential, verify all pods are updated, then revoke the old one. This avoids downtime during the transition.
- CSI Secrets Store Driver — Mounts secrets as volumes and supports
rotationPollIntervalfor in-place file updates without pod restarts. - Application-level reload — Some applications (e.g., Spring Boot with Actuator, nginx with
reloadsignal) can watch their config files and reload without restarting the process.
Summary
ConfigMaps and Secrets are fundamental Kubernetes primitives that Helm makes dynamic through templating. The key principles to remember:
- Use ConfigMaps for non-sensitive configuration and Secrets for credentials.
- Prefer volume mounts when the application supports live reloading; use environment variables for startup-only config.
- Never store plaintext secrets in
values.yaml. Use External Secrets Operator, Sealed Secrets, or the helm-secrets plugin. - Add checksum annotations to pod templates so configuration changes trigger automatic rollouts.
- Consider immutable ConfigMaps and Secrets for performance and safety in large clusters.
- Plan secret rotation from the start — it is far harder to retrofit than to build in.