The platform does not manage your application secrets. You create them, and you decide how they are stored and who can read them. A Kubernetes Secret holds credential data: a password, an API token, a TLS private key. Your workloads read Secrets to get those credentials. Keep the data safe everywhere it lives: at rest in etcd, in Git, on the API path, and at runtime in the pod. This page covers encrypting etcd first, keeping secret material out of Git, restricting who can read it with RBAC, mounting secrets as files, and pulling from an external store. ## Encrypt etcd first Turn on encryption at rest before anything else. etcd is the database where Kubernetes stores every object, including Secrets. By default, Kubernetes writes Secret data to etcd without encrypting it, so anyone who can read the raw database can read your secret values. etcd encryption is opt-in. It is off until you enable it: the `etcdEncryptionSecretRef.enabled` variable defaults to `false`. Once you enable it, the API server encrypts each Secret before writing it to etcd and decrypts it on read. Enabling encryption does not rewrite Secrets that already exist. A Secret written before you turned encryption on stays in plaintext in etcd until it is written again. To encrypt everything you already have, force a rewrite of all existing Secrets. The [Encrypt etcd](/docs/hetzner/apalla/security/encrypt-etcd) guide has the full steps, including the rewrite command and how to verify the raw bytes in etcd are encrypted. Encryption at rest is not access control. The API server decrypts on read, so anyone RBAC allows to `get` a Secret still receives the plaintext value. Encryption protects the raw database, such as a copied disk or a leaked etcd backup, not the API path. Pair it with the limits in [Restrict who can read Secrets with RBAC](#restrict-who-can-read-secrets-with-rbac). ## Keep secret material out of Git Do not commit plaintext Secrets to Git. A Kubernetes Secret stores its data base64-encoded, which is not encryption: anyone with the file can decode it. If you follow GitOps and keep your manifests in a repository, a committed Secret exposes its values to everyone who can read the repo, and it stays in the history even after you delete it. Sealed Secrets and External Secrets Operator both keep the reference in Git while the real secret stays out of it. **Sealed Secrets** encrypts a Secret into a `SealedSecret` object that only your cluster can decrypt. A controller in the cluster holds the private key. You commit the encrypted `SealedSecret` to Git, the controller decrypts it in the cluster, and it produces the normal Secret your workload reads. See [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets). **External Secrets Operator** keeps the secret in an external store, such as HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager, and syncs it into the cluster. You commit an `ExternalSecret` object that points at the entry in the store. The operator reads the store and creates the matching Secret. The secret value never lives in Git. See [External Secrets Operator](https://external-secrets.io). Once synced, the value lands as an ordinary Kubernetes Secret in etcd. The external store holds the source of truth, but the copy in the cluster gets no extra protection from it: the platform runs no in-path KMS that keeps Secrets envelope-encrypted in etcd, so the copy inherits etcd's at-rest status. Turn on etcd encryption before you rely on an external store. See [Data at rest and key custody](/docs/hetzner/apalla/security/data-at-rest-and-key-custody) for who holds which key. Pick one based on where you want the source of truth: in the cluster (Sealed Secrets) or in a dedicated secret store (External Secrets Operator). ## Restrict who can read Secrets with RBAC Limit `get` and `list` on Secrets to the workloads and people that need them. RBAC (role-based access control) is how Kubernetes decides who can do what. A subject that can `get` or `list` Secrets in a namespace can read every secret value in that namespace, because the API server returns the decrypted data. Grant the narrowest access that works. Scope a Role to a single namespace instead of a cluster-wide ClusterRole. Name the specific Secrets in `resourceNames` when a workload needs only a few. Avoid wildcard rules that grant `*` on `secrets`. ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: my-app name: read-db-credentials rules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["db-credentials"] verbs: ["get"] ``` This Role lets its subjects read one Secret named `db-credentials` in the `my-app` namespace, and nothing else. See [Manage access and tenancy](/docs/hetzner/apalla/security/manage-access-and-tenancy) for how to bind Roles to users and service accounts. ## Mount secrets as files, not environment variables Prefer a volume mount over `env` when you give a Secret to a container. Both work, but they expose the secret differently. An environment variable is easy to leak. Any process in the container can read the whole environment through `/proc//environ`. Crash handlers and logging libraries often dump environment variables into logs or error reports. Child processes inherit the environment, so a shell you exec into shows the value with `env`. A mounted file is narrower. The secret lands as a file in a directory you choose, readable only by processes with filesystem access to that path. It does not appear in the environment, so it is not inherited by child processes or dumped by a crash handler that prints the environment. A file mount also updates when the Secret changes; an environment variable is fixed at container start. ```yaml spec: containers: - name: app image: your/image:tag volumeMounts: - name: db-credentials mountPath: /etc/secrets readOnly: true volumes: - name: db-credentials secret: secretName: db-credentials ``` The application reads the value from a file under `/etc/secrets` instead of an environment variable. ## Rotate on custody change Rotate a secret whenever someone who held it should no longer have it. Replacing the value is the only way to take access back once a credential has been seen: a teammate leaves, a laptop that held a `kubeconfig` is lost, a token appears in a log, or a contractor finishes a job. Anyone who saw the old value still knows it, so the old value has to stop working. To rotate, create the new credential in its upstream system (the database, the cloud provider, the identity provider), update the Kubernetes Secret with the new value, and restart the workloads that read it so they pick it up. Then revoke the old credential upstream so it no longer authenticates. Rotate on a schedule as well, not only after an incident, so a value that leaked without your knowledge does not stay valid forever.