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.
This commit is contained in:
@@ -35,6 +35,7 @@ flake.nix
|
|||||||
│ ├── core.nix # system packages, locale, timezone
|
│ ├── core.nix # system packages, locale, timezone
|
||||||
│ ├── network.nix
|
│ ├── network.nix
|
||||||
│ ├── nix.nix
|
│ ├── nix.nix
|
||||||
|
│ ├── sops.nix # sops-nix decryption config, sops/age/ssh-to-age on PATH
|
||||||
│ └── users.nix
|
│ └── users.nix
|
||||||
├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*)
|
├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*)
|
||||||
│ ├── environments/ # per-service/app profiles
|
│ ├── environments/ # per-service/app profiles
|
||||||
@@ -43,10 +44,21 @@ flake.nix
|
|||||||
└── machines/<name>/
|
└── machines/<name>/
|
||||||
├── configuration.nix # machine-specific NixOS settings
|
├── configuration.nix # machine-specific NixOS settings
|
||||||
├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options
|
├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options
|
||||||
|
├── secrets.nix # sops.secrets declared for this host
|
||||||
├── disks.nix
|
├── disks.nix
|
||||||
└── hardware-configuration.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/<host>.yaml` holds
|
||||||
|
the encrypted values; `machines/<host>/secrets.nix` declares which secrets
|
||||||
|
that host exposes and consumes them via `config.sops.secrets.<name>.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
|
### Profile / Module Pattern
|
||||||
|
|
||||||
Every module under `modules/` follows the same structure:
|
Every module under `modules/` follows the same structure:
|
||||||
|
|||||||
@@ -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/<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`:
|
||||||
|
|
||||||
|
```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 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_<name>` anchor with that key, and a
|
||||||
|
`creation_rules` entry for `secrets/<name>.yaml` including both the new
|
||||||
|
host key and `*admin_finn`.
|
||||||
|
3. Create `machines/<name>/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/<file>.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/<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, 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/<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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user