A Kubernetes Secret is not secret. It is base64. Anyone who can read the Secret object, or read etcd underneath it, recovers the value one `base64 -d` later. That single fact decides how you handle everything else on this page. > [!WARNING] > Two things are on you. 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. Base64 is packaging, not a security boundary. ## 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 what you would not paste into a support ticket: passwords, tokens, keys. The Secret is the path with RBAC gating and etcd encryption behind it; that is the tiebreaker. ## 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 ``` Env vars are the simplest thing that works. But a volume-mounted ConfigMap has one property env vars never will: it **updates live** when you edit it (after a short propagation delay), while an env var is frozen for the pod's whole life and only changes on a restart. If your application rereads its config file, mount it. One pitfall: `subPath` mounts a single key as one file without shadowing the rest of the directory, exactly what you use first. It also quietly kills the live update. That silent loss of live updates is a common cause of a config change that never lands. 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 You deploy from Git. So a raw Secret cannot go in the repo, or the credential lives in history forever. 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.