8.2 KiB
name, description
| name | description |
|---|---|
| nix-config-secrets | Use when adding, changing, renaming, or removing an agenix secret, wiring one into a host, or fixing a decryption or activation failure in this repo. |
Working with secrets
Secrets are age-encrypted files in the private repository ~/codes/nix-secrets
(git@github.com:ryan4yin/nix-secrets.git), pulled in as the mysecrets flake input and declared
here in secrets/nixos.nix and secrets/darwin.nix. No secret value or ciphertext is ever stored
in this public repository.
Read secrets/README.md for the concepts and the recipient rule.
Core rules
- Never read a decrypted secret. Reference its path; do not
cat, copy, or print it. Prove a change with metadata (mode, owner, timestamps), never with content. - Three steps, in order. The private repository changes first, then this repository's
flake.lockmovesmysecretsto that commit, then the declaration and consumer change here.file = "${mysecrets}/x.age"resolves against the locked revision, so a declaration that lands before the lock bump points at a file that does not exist yet. - Every secret stays decryptable by the desktops. A recipient set is
desktop_keys ++ <the hosts that need it>;recovery_keyis a member ofdesktop_keys. The one exception isrestic-password-desktop.age, readable bydesktop_keysalone. - A consumer sits behind the same gate as its secret. A secret declared under
modules.secrets.desktoponly exists on desktops, so only a desktop-gated module may reference it. Desktops and servers are mutually exclusive:secrets/nixos.nixasserts that a host never enablesdesktoptogether with a server group, so a value both sides need takes a deliberate choice rather than enabling both groups. - Least privilege on the decrypted file. Pick the narrowest mode that works, and set the owner together with the mode.
1. Where the pieces live
| Piece | Location |
|---|---|
Recipient list, encrypted files, agenix CLI |
~/codes/nix-secrets (private; git@github.com:ryan4yin/nix-secrets.git) |
| The pinned revision of that repository | flake.lock, input mysecrets |
Declaration: file, mode/owner, /etc copy |
secrets/nixos.nix, secrets/darwin.nix |
| Which host gets which group | modules.secrets.<group>.enable in outputs/<system>/src/<name>.nix or a host module |
| Consumers | modules reading config.age.secrets."<name>".path (default /run/agenix/<name>) |
| Decryption key | age.identityPaths: the host's SSH host key; /persistent/etc/ssh/... on a preservation host |
The private repository groups the .age files: desktop/ (only desktops decrypt them), server/
(any server), certs/, and public/.
2. Add or change a secret
Do the mechanical work yourself: edit secrets.nix in ~/codes/nix-secrets, run the
non-interactive steps, bump the lock, and add the declaration and consumer. Hand back only what
needs a human: the edit/rekey recipes in secrets/Justfile (they
handle key material and $EDITOR) and anything else under sudo. Commits and pushes follow the
global git rules.
- In
~/codes/nix-secrets, add the file underdesktop/orserver/with a matchingsecrets.nixentry, keyed by that exact path (e.g."./desktop/xxx.age"), and the recipient set from core rule 3. Then ask the user to create or edit it on a desktop with the command in §3. - Here:
just upp mysecrets(commits the lock) ornix flake update mysecrets(leaves it for you to commit).git diff flake.lockshould show onlymysecretsmoving. - Declare it in
secrets/nixos.nixorsecrets/darwin.nixunder the rightmodules.secrets.<group>gate, with a mode/owner preset (§4). - Point the consumer at the runtime path, from a module behind the same gate (core rule 4).
just testandjust build-host <host>, deploy, then verify as in §5.
Changing only a secret's value is steps 1, 2, and the deploy.
3. The private repository
The agenix operations run against ~/codes/nix-secrets. Their single source is
secrets/Justfile, which pins the identity, option order, and
$EDITOR; run them from this repository's root and keep each path identical to its key in
secrets.nix:
just -f secrets/Justfile edit ./desktop/xxx.age # edit or create (interactive)
just -f secrets/Justfile rekey # re-encrypt after a recipient change
edit passes EDITOR=hx to the root agenix process, because sudo resets the environment and
the invoking user's EDITOR would not reach it; do not add sudo -E or set EDITOR yourself.
The repository keeps a single amended commit: git commit --amend -a --no-edit,
git reflog expire --expire-unreachable=now --all, git gc --prune=now, then force push. Treat
amend and force push as impactful and get authorization first.
4. Modes and ownership
Use one of the presets in secrets/nixos.nix (noaccess, high_security, user_readable); the
table and the environment.etc copy trap are in
secrets/README.md. The rule to remember:
whenever an environment.etc entry sets mode, it also sets user. Never widen a mode to make
a root-owned copy readable.
5. Verify without reading
stat -c '%a %U:%G' /run/agenix/<name> # mode and owner, not content (the default path)
ls -l /run/agenix/ # /etc/agenix/ holds only the environment.etc copies
journalctl -b | grep -5 agenix # NixOS
tail -n 100 /Library/Logs/org.nixos.activate-agenix.stderr.log # macOS
A successful activation is silent. Check that the access changed as intended (the mode and owner you set, an old copy gone) instead of assuming activation did it.
6. Remove or rename a secret
-
Search all references before changing it:
grep -Rni '<secret-name>' --include='*.nix' --include='*.toml' .Check
secrets/,home/,modules/,hosts/, outputs, tests, and the private repository. -
Delete the
age.secretsentry, itsenvironment.etcplacement, and every consumer in one change; a declaration whose file no longer exists breaks activation. -
Remove it from
secrets.nixand delete the file in the private repository, then bump the lock. -
A rename changes the attribute name and every consumer. The
.agefilename is separate.
7. Failure modes
- Decryption fails on one host: its host key is not a recipient. Add its
/etc/ssh/ssh_host_ed25519_key.pubtosecrets.nix, rekey (§3), push, bump the lock, redeploy. - Eval or activation cannot find the file: the lock still points at a
mysecretsrevision without it (core rule 2). permission deniedin a user service: it reads a root-only secret. Fix that secret's owner; do not widen the mode.- A preservation host cannot decrypt at boot:
age.identityPathsmust use the/persistent/etc/ssh/...path, which exists before preservation mounts/etc. - The first macOS activation fails on the
/etc/agenixcopies, because they run beforeactivate-agenixhas decrypted anything. Run it again.
Why these rules exist
4909f635 security: scope privileges and stop a world-readable secret copy (#335)- theenvironment.etccopy trap in step 3.c8e76cef fix(darwin): agenix - remove non-exist secret- a declaration left behind after its file was deleted.4211d18a- a sharedmodules/nixos/basemodule includednix-access-tokens, which not every host had; the reference moved tomodules/nixos/desktop/nix.nix(core rule 4).260da1ee chore: rename the nushell secret, and forbid reading decrypted secrets- the no-reading rule.