Secrets without plaintext
No secret sits in Git as readable text. Two tools handle the two halves.
In the cluster: Sealed Secrets
A controller in the cluster holds a private key. The matching public key encrypts:
kubectl create secret generic app \
--from-literal=KEY=value --dry-run=client -o yaml \
| kubeseal --format yaml > sealed-secret.yamlThe output is safe to commit. On sync the controller decrypts it into a real Secret.
Each cluster runs its own controller with its own sealing key, so a sealed secret is sealed for the specific cluster that will run it — the same plaintext sealed for two clusters is two different files.
On the host: SOPS with age
Host secrets, such as tunnel credentials, stay encrypted in the NixOS repo and are decrypted at activation. The age identity is the machine's own SSH host key.
sops secrets/homelab.yaml # edit; re-encrypts on saveThe rule
Plaintext never enters Git. If a value has to live in the repo, it is encrypted first.
Back up the sealing keys. Without them, sealed secrets cannot be opened again.
Since writing this
I moved the cluster half to a managed store: Bitwarden Secrets Manager, fronted by External Secrets Operator. The values now live outside the cluster, so a cluster wipe does not lose them, and rotating a credential is editing it there rather than re-sealing a blob. The host half is unchanged. The reasoning is in Choosing the architecture .