Operator runbook for adding a secret, adding a new host, and rotating keys, plus a pointer from CLAUDE.md's architecture overview.
3.5 KiB
Secrets (sops-nix)
Secrets are encrypted with sops and decrypted at activation by sops-nix. Each host decrypts with its own SSH host key (converted to an age key in-memory — no separate host private key is ever stored on disk).
Tooling (sops, age, ssh-to-age) is available via nix develop in this repo,
and installed system-wide on every machine via machines/core/sops.nix.
Adding a secret to an existing host
nix develop -c sops secrets/<host>.yaml
This opens $EDITOR on the decrypted content. Add your key, save, quit — sops
re-encrypts on write. Then declare it in machines/<host>/secrets.nix:
sops.secrets.my-secret = {
owner = "someuser"; # defaults to root
group = "someuser";
mode = "0400";
restartUnits = [ "foo.service" ]; # if a running service needs to pick it up
};
Consume it in the owning module as config.sops.secrets.my-secret.path
(resolves to /run/secrets/my-secret) — never inline the decrypted value into
a Nix string, since that copies it into the world-readable /nix/store.
Then:
nix build '.#nixosConfigurations.<host>.config.system.build.toplevel'
sudo nixos-rebuild test --flake '.#<host>'
# verify, then:
sudo nixos-rebuild switch --flake '.#<host>'
The secrets file must be git added before nix build — untracked files
are invisible to flake evaluation, and sops.validateSopsFiles needs it in the
Nix store.
Adding a new host
- On the new host:
ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pubto get its age public key. - In
.sops.yaml: add a&host_<name>anchor with that key, and acreation_rulesentry forsecrets/<name>.yamlincluding both the new host key and*admin_finn. - Create
machines/<name>/secrets.nix, add it to that machine'sconfiguration.niximports. - If the host needs to read an existing shared file, run
sops updatekeys secrets/<file>.yamlafter step 2 so it's re-encrypted to include the new recipient.
Rotating keys
# after editing .sops.yaml (new/removed recipient):
nix develop -c sops updatekeys secrets/<file>.yaml
for every file the change affects. updatekeys requires you to already be
able to decrypt the file — this is why *admin_finn is a recipient on every
rule, as the standing fallback.
Reinstalling a host regenerates its SSH host key, which changes its age
identity. Any file previously encrypted to that host must be updatekeys'd
with the new key, or the host's next activation fails to decrypt.
Where private keys live
- Operator (you):
~/.config/sops/age/keys.txt— sops' default lookup path, soSOPS_AGE_KEY_FILEnever needs to be set. Never committed. Back this up out-of-band (password manager, offline copy) — if it's lost, only the surviving host keys can still decrypt their own files, and any file that was only ever readable by an operator-only rule becomes permanently unreadable. - Hosts:
/etc/ssh/ssh_host_ed25519_key— already managed by NixOS, converted to age in-memory at activation time. Nothing extra to back up.
Layout
.sops.yaml # age recipients + creation rules (which keys can decrypt which files)
secrets/<host>.yaml # per-host encrypted secrets (default sopsFile for that host)
machines/core/sops.nix # applied to every machine: decryption config, sops/age/ssh-to-age on PATH
machines/<host>/secrets.nix # per-host: which secrets exist, owner/group/mode