A Kubernetes Secret is not encrypted by default; its values are only base64-encoded. Anyone who can read the Secret object, or the underlying etcd datastore, can recover the plaintext with a single `base64 -d`. Everything else on this page follows from that limitation. > [!WARNING] > Gate Secrets with RBAC so only the ServiceAccounts that need a value can read it, and [encrypt etcd at rest](/docs/hetzner/apalla/security/data-at-rest-and-key-custody) so the plaintext is not sitting in the datastore for anyone with disk or backup access. ## ConfigMap or Secret Both objects hold key-value data; the difference is intent. A **ConfigMap** carries non-sensitive config: feature flags, URLs, a whole config file. A **Secret** carries values you would not want stored unencrypted: passwords, tokens, keys. The Secret is the path with RBAC gating and [etcd encryption](/docs/hetzner/apalla/clusters/configure/encrypt-etcd) behind it, so choose it whenever the value is sensitive. ## Inject as env vars or files ```yaml # Inject as environment variables envFrom: - configMapRef: name: app-config # every key in the ConfigMap becomes a var env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-creds # pull one Secret key into one named var key: password ``` ```yaml # Or mount them as files volumes: - name: config configMap: name: app-config volumeMounts: - name: config mountPath: /etc/app # each key lands as a file under this path ``` Environment variables are the simplest option, but they have one limitation: their values are fixed for the lifetime of the pod and only change when it restarts. A volume-mounted ConfigMap, by contrast, **updates live** after you edit it (following a short propagation delay). If your application rereads its config file at runtime, mount the config as a volume rather than injecting it as environment variables. One pitfall: `subPath` mounts a single key as one file without shadowing the rest of the directory, exactly what you use first. It also disables live updates, a common reason a config change never takes effect. Check for a subPath mount first. ## Immutable ConfigMaps and Secrets Set `immutable: true` when the content must not change under a running application. The API server then refuses edits, which stops accidental updates, and the kubelet stops watching the object entirely, which is real load off the control plane on a cluster with thousands of pods. To change one, create a new object under a new name and roll the workload onto it. ## Keeping secrets out of Git Because you deploy from Git, a raw Secret cannot go in the repo, or the credential stays in the history permanently. Two patterns keep GitOps intact: - **Sealed Secrets** encrypts the value against a key that lives only in the cluster. The blob is safe to commit; a controller unseals it in place. Rotate that key and old sealed values stop decrypting, so plan for it. - **External Secrets** keeps the value in an external manager and commits only a pointer, so Git never touches the plaintext. Sealed Secrets suits a self-contained cluster; External Secrets suits organizations already running a central vault. Pick one _before_ the first manifest lands in Git; retrofitting means rewriting history. See [Deploy apps from Git with Argo CD](/docs/hetzner/apalla/workloads/delivery/deploy-apps-from-git-argo-cd). To confirm it landed, `kubectl exec -- printenv DB_PASSWORD`. A pod that never starts its container usually means a wrong `key` in the `secretKeyRef`, or a Secret in a different namespace than the pod: Secrets do not cross namespaces.