From 621ca5dde6d8f499ca4e41e68401467e17098767 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 15:43:50 +0200 Subject: [PATCH] docs: sops-nix secrets workflow Operator runbook for adding a secret, adding a new host, and rotating keys, plus a pointer from CLAUDE.md's architecture overview. --- CLAUDE.md | 12 +++++++ docs/secrets.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 docs/secrets.md diff --git a/CLAUDE.md b/CLAUDE.md index f480c60..fcd95e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,7 @@ flake.nix │ ├── core.nix # system packages, locale, timezone │ ├── network.nix │ ├── nix.nix + │ ├── sops.nix # sops-nix decryption config, sops/age/ssh-to-age on PATH │ └── users.nix ├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*) │ ├── environments/ # per-service/app profiles @@ -43,10 +44,21 @@ flake.nix └── machines// ├── configuration.nix # machine-specific NixOS settings ├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options + ├── secrets.nix # sops.secrets declared for this host ├── disks.nix └── hardware-configuration.nix ``` +### Secrets (sops-nix) + +Secrets are encrypted with sops, decrypted per-host from that host's SSH host +key. `.sops.yaml` defines who can decrypt what; `secrets/.yaml` holds +the encrypted values; `machines//secrets.nix` declares which secrets +that host exposes and consumes them via `config.sops.secrets..path`. +Always `git add` a secrets file before `nix build` — untracked files are +invisible to flake evaluation. Full workflow (adding a secret, adding a host, +rotating keys): see [`docs/secrets.md`](docs/secrets.md). + ### Profile / Module Pattern Every module under `modules/` follows the same structure: diff --git a/docs/secrets.md b/docs/secrets.md new file mode 100644 index 0000000..dc098fe --- /dev/null +++ b/docs/secrets.md @@ -0,0 +1,92 @@ +# Secrets (sops-nix) + +Secrets are encrypted with [sops](https://github.com/getsops/sops) and decrypted +at activation by [sops-nix](https://github.com/Mic92/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/.yaml +``` + +This opens `$EDITOR` on the decrypted content. Add your key, save, quit — sops +re-encrypts on write. Then declare it in `machines//secrets.nix`: + +```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..config.system.build.toplevel' +sudo nixos-rebuild test --flake '.#' +# verify, then: +sudo nixos-rebuild switch --flake '.#' +``` + +**The secrets file must be `git add`ed before `nix build`** — untracked files +are invisible to flake evaluation, and `sops.validateSopsFiles` needs it in the +Nix store. + +## Adding a new host + +1. On the new host: `ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub` to get its + age public key. +2. In `.sops.yaml`: add a `&host_` anchor with that key, and a + `creation_rules` entry for `secrets/.yaml` including both the new + host key and `*admin_finn`. +3. Create `machines//secrets.nix`, add it to that machine's + `configuration.nix` imports. +4. If the host needs to read an existing shared file, run + `sops updatekeys secrets/.yaml` after 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/.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, so `SOPS_AGE_KEY_FILE` never 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/.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//secrets.nix # per-host: which secrets exist, owner/group/mode +```