Lesson 06 Secrets & ConfigMaps
Theme:

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:

MethodHowLive Updates?
Environment variablesenvFrom or individual configMapKeyRefNo — requires pod restart
Volume mountvolumes + volumeMountsYes — 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:

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:

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:

Summary

ConfigMaps and Secrets are fundamental Kubernetes primitives that Helm makes dynamic through templating. The key principles to remember:

  1. Use ConfigMaps for non-sensitive configuration and Secrets for credentials.
  2. Prefer volume mounts when the application supports live reloading; use environment variables for startup-only config.
  3. Never store plaintext secrets in values.yaml. Use External Secrets Operator, Sealed Secrets, or the helm-secrets plugin.
  4. Add checksum annotations to pod templates so configuration changes trigger automatic rollouts.
  5. Consider immutable ConfigMaps and Secrets for performance and safety in large clusters.
  6. Plan secret rotation from the start — it is far harder to retrofit than to build in.