Secrets: sops-nix
Every secret in this repo is committed encrypted with sops + age, and decrypted only where it’s needed. Nothing sensitive ever lands in the repo (or in CI logs) as plaintext.
The recipients
Defined in .sops.yaml. Each secret is encrypted to one or more age recipients:
| Anchor | Key lives at | Role |
|---|---|---|
admin | ~/.config/sops/age/keys.txt (the Mac) | edit secrets locally |
avocado | /var/lib/sops-nix/key.txt (the box) | decrypt at activation/boot |
The host key is provisioned out-of-band — it is never in the repo. That’s the whole point: the encrypted files are safe to commit because only the box (or you) holds a key that can open them.
Which file is encrypted to whom
creation_rules in .sops.yaml map each path to its recipients:
| File | Recipients | Contents |
|---|---|---|
secrets/avocado.yaml | admin + avocado | user password hash, Tailscale auth key, k3s token |
secrets/ssh_id_ed25519 | admin + avocado | user’s SSH private key (binary) |
secrets/cloudflared_credentials.json | admin + avocado | tunnel credentials (binary) |
secrets/monitoring.enc.yaml | admin + avocado | Grafana admin password, ntfy token |
secrets/care.enc.yaml | admin + avocado | CARE k8s Secret: Django key, JWKS, Postgres + MinIO credentials |
secrets/care-teleicu.enc.yaml | admin + avocado | TeleICU gateway k8s Secret: Django key, gateway JWKS, Postgres + S3 credentials |
secrets/kite.enc.yaml | admin + avocado | Kite k8s Secret: JWT + encrypt keys, GitHub OAuth app id/secret, break-glass password |
secrets/settle-up-mcp.enc.yaml | admin + avocado | Settle Up MCP k8s Secret: account email + password, live Firebase Web API key, MCP bearer token |
How the box consumes secrets
flowchart LR
file[(secrets/avocado.yaml - encrypted)]
key[avocado age key - /var/lib/sops-nix/key.txt]
file -->|sops-nix at activation| dec{decrypt}
key --> dec
dec --> pw[run-secrets: user hashed-password]
dec --> ts[tailscale auth-key]
dec --> k3s[k3s token]
dec --> ssh[rithviknishad ssh key - 0600]
modules/sops.nix sets secrets/avocado.yaml as the default source and the host key as the decryptor, then declares individual secrets:
users/rithviknishad/hashed-password— markedneededForUsersso it is decrypted early enough to create the account (before normal/run/secretsis mounted). Wired into the user viahashedPasswordFile.rithviknishad/ssh_id_ed25519— its own binary sops file, decrypted to/run/secrets/...owned by the user (mode0600) and referenced by the Home Manager ssh module.
Other modules declare the secrets they need the same way: tailscale/auth-key (tailscale.nix), k3s/token (k3s.nix), and cloudflared/credentials (cloudflared.nix).
Editing secrets
Always work inside nix develop (it sets SOPS_AGE_KEY_FILE). The justfile wraps the common operations:
just secrets # edit secrets/avocado.yaml
just secrets-show # print decrypted (mind your screen)
just secrets-rekey # re-encrypt after changing recipients in .sops.yaml
just passwd # generate a SHA-512 hash to paste in (mkpasswd -m sha-512)
There is also a parallel set of recipes for the monitoring secret file (just mon-secrets*).
Deploy-time decryption (monitoring)
The Grafana admin password is not read by the box. Instead just mon-deploy decrypts secrets/monitoring.enc.yaml into the gitignored k8s/monitoring/values-secret.yaml on the admin machine, right before helmfile sync, and it’s never committed. See Monitoring.