mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
refactor(secrets): follow the reorganized nix-secrets layout (#344)
This commit is contained in:
1 parent
969a20ac7b
commit
1a10e65221
7 files changed
+83
-48
No files matched your search
@@ -9,11 +9,10 @@ description:
|
||||
|
||||
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 is ever stored in this
|
||||
repository.
|
||||
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](../../../secrets/README.md) for the private-repository workflow and the
|
||||
recipient rule.
|
||||
Read [secrets/README.md](../../../secrets/README.md) for the concepts and the recipient rule.
|
||||
|
||||
## Core rules
|
||||
|
||||
@@ -45,26 +44,45 @@ recipient rule.
|
||||
| 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: `sudo agenix -e`/`-r` (interactive, and they handle key material) and anything else
|
||||
under `sudo`. Commits and pushes follow the global git rules.
|
||||
needs a human: the `edit`/`rekey` recipes in [`secrets/Justfile`](../../../secrets/Justfile) (they
|
||||
handle key material and `$EDITOR`) and anything else under `sudo`. Commits and pushes follow the
|
||||
global git rules.
|
||||
|
||||
1. In `~/codes/nix-secrets`, add the file to `secrets.nix` with the recipient set from core rule 3.
|
||||
Then ask the user to create or edit it on a desktop:
|
||||
`sudo agenix -e ./xxx.age -i /etc/ssh/ssh_host_ed25519_key`.
|
||||
1. In `~/codes/nix-secrets`, add the file under `desktop/` or `server/` with a matching
|
||||
`secrets.nix` entry, 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.
|
||||
2. Here: `just upp mysecrets` (commits the lock) or `nix flake update mysecrets` (leaves it for you
|
||||
to commit). `git diff flake.lock` should show only `mysecrets` moving.
|
||||
3. Declare it in `secrets/nixos.nix` or `secrets/darwin.nix` under the right
|
||||
`modules.secrets.<group>` gate, with a mode/owner preset (step 3).
|
||||
`modules.secrets.<group>` gate, with a mode/owner preset (§4).
|
||||
4. Point the consumer at the runtime path, from a module behind the same gate (core rule 4).
|
||||
5. `just test` and `just build-host <host>`, deploy, then verify as in step 4.
|
||||
5. `just test` and `just build-host <host>`, deploy, then verify as in §5.
|
||||
|
||||
Changing only a secret's value is steps 1, 2, and the deploy.
|
||||
|
||||
## 3. Modes and ownership
|
||||
## 3. The private repository
|
||||
|
||||
The agenix operations run against `~/codes/nix-secrets`. Their single source is
|
||||
[`secrets/Justfile`](../../../secrets/Justfile), which pins the identity and option order; run them
|
||||
from this repository's root and keep each path identical to its key in `secrets.nix`:
|
||||
|
||||
```bash
|
||||
just -f secrets/Justfile edit ./desktop/xxx.age # edit or create (interactive)
|
||||
just -f secrets/Justfile rekey # re-encrypt after a recipient change
|
||||
```
|
||||
|
||||
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
|
||||
@@ -72,7 +90,7 @@ table and the `environment.etc` copy trap are in
|
||||
**whenever an `environment.etc` entry sets `mode`, it also sets `user`.** Never widen a mode to make
|
||||
a root-owned copy readable.
|
||||
|
||||
## 4. Verify without reading
|
||||
## 5. Verify without reading
|
||||
|
||||
```bash
|
||||
stat -c '%a %U:%G' /run/agenix/<name> # mode and owner, not content (the default path)
|
||||
@@ -84,7 +102,7 @@ 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.
|
||||
|
||||
## 5. Remove or rename a secret
|
||||
## 6. Remove or rename a secret
|
||||
|
||||
- Search all references before changing it:
|
||||
|
||||
@@ -99,11 +117,10 @@ you set, an old copy gone) instead of assuming activation did it.
|
||||
- Remove it from `secrets.nix` and delete the file in the private repository, then bump the lock.
|
||||
- A rename changes the attribute name and every consumer. The `.age` filename is separate.
|
||||
|
||||
## 6. Failure modes
|
||||
## 7. Failure modes
|
||||
|
||||
- **Decryption fails on one host:** its host key is not a recipient. Add its
|
||||
`/etc/ssh/ssh_host_ed25519_key.pub` to `secrets.nix`, rekey with
|
||||
`sudo agenix -r -i /etc/ssh/ssh_host_ed25519_key` on a desktop, push, bump the lock, redeploy.
|
||||
`/etc/ssh/ssh_host_ed25519_key.pub` to `secrets.nix`, rekey (§3), push, bump the lock, redeploy.
|
||||
- **Eval or activation cannot find the file:** the lock still points at a `mysecrets` revision
|
||||
without it (core rule 2).
|
||||
- **`permission denied` in a user service:** it reads a root-only secret. Fix that secret's owner;
|
||||
|
||||
@@ -13,7 +13,9 @@ deployments. Keep repository guidance here; reusable global rules live in `agent
|
||||
- `hosts/` contains host-specific configuration; `outputs/<system>/src/` wires hosts into outputs.
|
||||
- `vars/` and `lib/` provide shared values and helpers. Use `myvars` and existing abstractions
|
||||
instead of hardcoding usernames or paths.
|
||||
- `secrets/` contains agenix definitions; secret material also comes from a private external repo.
|
||||
- `secrets/` contains agenix definitions. The encrypted `.age` files live in the private
|
||||
`~/codes/nix-secrets` repository, never in this public one — never copy a `.age` file here. Use
|
||||
the `nix-config-secrets` skill.
|
||||
- `overlays/` and `hardening/` hold package overlays and hardened (nixpak/bwrap) wrappers.
|
||||
- Desktop (Niri and Noctalia) config is mostly out-of-store symlinks that hot-reload without a
|
||||
rebuild; see the desktop skill below before changing it.
|
||||
@@ -110,8 +112,8 @@ repository, and cross-link them — no need to be told.
|
||||
networking, VM images, or cluster addons usually change both repositories together.
|
||||
- `~/codes/containers` — container images consumed by k8s-gitops; a tag bump is often a two-repo
|
||||
change.
|
||||
- `~/codes/nix-secrets` — agenix files and recipients for secrets declared here (see the
|
||||
`nix-config-secrets` skill).
|
||||
- `~/codes/nix-secrets` — the private agenix store behind `secrets/` and the `mysecrets` input:
|
||||
ciphertext and recipient rules for the secrets declared here.
|
||||
- `wallpapers`, `nur-packages`, `pyclipsync`, `nu_scripts` — flake inputs; bump with
|
||||
`just upp <input>` after their source changes, do not edit them from here.
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -484,10 +484,10 @@
|
||||
"mysecrets": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1790913443,
|
||||
"narHash": "sha256-3KvrF3j19qo90q0frvQnU4/Lirh1mb0dk8UA0KUrtog=",
|
||||
"lastModified": 1790915701,
|
||||
"narHash": "sha256-ES+PUPSCJBE4pwhTBwgKsoBit3oXjrI/OJt5xxngeT4=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "bc17c93809bc029f2901b86a5e3306a6ef1be4d3",
|
||||
"rev": "37e3c3ee87ac649fc52f9314db1cdf672a3c62b0",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "ssh://git@github.com/ryan4yin/nix-secrets.git"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# just is a command runner; Justfile is like Makefile but simpler.
|
||||
# The agenix operations run against the private secrets repository at ~/codes/nix-secrets.
|
||||
# Uses nushell: nix shell nixpkgs#just nixpkgs#nushell
|
||||
set shell := ["nu", "-c"]
|
||||
set working-directory := "../../codes/nix-secrets"
|
||||
|
||||
identity := "/etc/ssh/ssh_host_ed25519_key"
|
||||
|
||||
# List all recipes
|
||||
default:
|
||||
@just --list --justfile {{justfile()}}
|
||||
|
||||
# Edit or create a secret (interactive); pipe the plaintext to write non-interactively
|
||||
edit file:
|
||||
sudo -E agenix -i {{identity}} -e {{quote(file)}}
|
||||
|
||||
# Re-encrypt every secret after adding or changing a recipient key
|
||||
[confirm("Re-encrypt every secret in ~/codes/nix-secrets?")]
|
||||
rekey:
|
||||
sudo -E agenix -r -i {{identity}}
|
||||
sudo chown -R ryan:ryan *
|
||||
+5
-10
@@ -107,16 +107,11 @@ in
|
||||
}
|
||||
```
|
||||
|
||||
3. Create and edit the secret file `xxx.age` interactively using the following command:
|
||||
3. Create and edit the secret file `xxx.age` with the [`Justfile`](./Justfile) `edit` recipe
|
||||
(interactive, from the repository root); pipe the plaintext to write it non-interactively:
|
||||
|
||||
```shell
|
||||
sudo agenix -e ./xxx.age -i /etc/ssh/ssh_host_ed25519_key
|
||||
```
|
||||
|
||||
Alternatively, you can encrypt an existing file to `xxx.age` using the following command:
|
||||
|
||||
```shell
|
||||
cat xxx | sudo agenix -e ./xxx.age -i /etc/ssh/ssh_host_ed25519_key
|
||||
just -f secrets/Justfile edit ./xxx.age
|
||||
```
|
||||
|
||||
`agenix` will encrypt the file with all the public keys we defined in `secrets.nix`, so all the
|
||||
@@ -214,8 +209,8 @@ the `age.secrets.<name>.path` argument, which defaults to `/run/agenix/<name>`.
|
||||
1. `cat` the system-level public key(`/etc/ssh/ssh_host_ed25519_key.pub`) of the new host, and send
|
||||
it to an old host which has already been configured.
|
||||
2. On the old host:
|
||||
1. Add the public key to `secrets.nix`, and rekey all the secrets via
|
||||
`sudo agenix -r -i /etc/ssh/ssh_host_ed25519_key`.
|
||||
1. Add the public key to `secrets.nix`, then rekey all the secrets:
|
||||
`just -f secrets/Justfile rekey`.
|
||||
2. Commit and push the changes to `nix-secrets`.
|
||||
3. On the new host:
|
||||
1. Clone this repo and run `nixos-rebuild switch` to deploy it, all the secrets will be decrypted
|
||||
|
||||
+5
-5
@@ -49,7 +49,7 @@
|
||||
|
||||
# .age means the decrypted file is still encrypted by age(via a passphrase)
|
||||
"ryan4yin-gpg-subkeys.priv.age" = {
|
||||
file = "${mysecrets}/ryan4yin-gpg-subkeys-2024-01-27.priv.age.age";
|
||||
file = "${mysecrets}/desktop/ryan4yin-gpg-subkeys-2024-01-27.priv.age.age";
|
||||
}
|
||||
// noaccess;
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
# ---------------------------------------------
|
||||
|
||||
"rclone.conf" = {
|
||||
file = "${mysecrets}/rclone.conf.age";
|
||||
file = "${mysecrets}/desktop/rclone.conf.age";
|
||||
}
|
||||
// high_security;
|
||||
|
||||
"nix-access-tokens" = {
|
||||
file = "${mysecrets}/nix-access-tokens.age";
|
||||
file = "${mysecrets}/server/nix-access-tokens.age";
|
||||
}
|
||||
# access-token needs to be readable by the user running the `nix` command
|
||||
// user_readable;
|
||||
@@ -73,13 +73,13 @@
|
||||
# ---------------------------------------------
|
||||
|
||||
"ssh-key-romantic" = {
|
||||
file = "${mysecrets}/ssh-key-romantic.age";
|
||||
file = "${mysecrets}/desktop/ssh-key-romantic.age";
|
||||
}
|
||||
// user_readable;
|
||||
|
||||
# decrypted secret: AI agents must not read it, only reference the path
|
||||
"nushell-secrets.nu" = {
|
||||
file = "${mysecrets}/nushell-secrets.nu.age";
|
||||
file = "${mysecrets}/desktop/nushell-secrets.nu.age";
|
||||
}
|
||||
// user_readable;
|
||||
};
|
||||
|
||||
+10
-10
@@ -72,7 +72,7 @@ in
|
||||
# secrets that are used by all nixos hosts
|
||||
age.secrets = {
|
||||
"nix-access-tokens" = {
|
||||
file = "${mysecrets}/nix-access-tokens.age";
|
||||
file = "${mysecrets}/server/nix-access-tokens.age";
|
||||
}
|
||||
# access-token needs to be readable by the user running the `nix` command
|
||||
// user_readable;
|
||||
@@ -95,14 +95,14 @@ in
|
||||
|
||||
# .age means the decrypted file is still encrypted by age(via a passphrase)
|
||||
"ryan4yin-gpg-subkeys.priv.age" = {
|
||||
file = "${mysecrets}/ryan4yin-gpg-subkeys-2024-01-27.priv.age.age";
|
||||
file = "${mysecrets}/desktop/ryan4yin-gpg-subkeys-2024-01-27.priv.age.age";
|
||||
}
|
||||
// noaccess;
|
||||
|
||||
# Used only by NixOS Modules
|
||||
|
||||
"rclone.conf" = {
|
||||
file = "${mysecrets}/rclone.conf.age";
|
||||
file = "${mysecrets}/desktop/rclone.conf.age";
|
||||
}
|
||||
// high_security;
|
||||
|
||||
@@ -111,13 +111,13 @@ in
|
||||
# ---------------------------------------------
|
||||
|
||||
"ssh-key-romantic" = {
|
||||
file = "${mysecrets}/ssh-key-romantic.age";
|
||||
file = "${mysecrets}/desktop/ssh-key-romantic.age";
|
||||
}
|
||||
// user_readable;
|
||||
|
||||
# decrypted secret: AI agents must not read it, only reference the path
|
||||
"nushell-secrets.nu" = {
|
||||
file = "${mysecrets}/nushell-secrets.nu.age";
|
||||
file = "${mysecrets}/desktop/nushell-secrets.nu.age";
|
||||
}
|
||||
// user_readable;
|
||||
};
|
||||
@@ -232,13 +232,13 @@ in
|
||||
# the backup server holds the homelab one, so a shared password would let
|
||||
# that server decrypt desktop data.
|
||||
age.secrets."restic-password" = {
|
||||
file = "${mysecrets}/restic-password-desktop.age";
|
||||
file = "${mysecrets}/desktop/restic-password-desktop.age";
|
||||
mode = "0400";
|
||||
owner = "root";
|
||||
};
|
||||
# credentials for the restic REST server that receives this backup
|
||||
age.secrets."restic-rest-credentials" = {
|
||||
file = "${mysecrets}/restic-rest-credentials.age";
|
||||
file = "${mysecrets}/desktop/restic-rest-credentials.age";
|
||||
mode = "0400";
|
||||
owner = "root";
|
||||
};
|
||||
@@ -248,14 +248,14 @@ in
|
||||
# the homelab's restic repository password, shared by the homelab hosts
|
||||
# (this host holds it, so it can copy/verify homelab repositories)
|
||||
age.secrets."restic-password" = {
|
||||
file = "${mysecrets}/restic-password-homelab.age";
|
||||
file = "${mysecrets}/server/restic-password-homelab.age";
|
||||
mode = "0400";
|
||||
owner = "root";
|
||||
};
|
||||
# htpasswd for the restic REST server this host runs; the service runs as
|
||||
# the unprivileged `restic` user, which must be able to read it
|
||||
age.secrets."restic-rest-htpasswd" = {
|
||||
file = "${mysecrets}/restic-rest-htpasswd.age";
|
||||
file = "${mysecrets}/server/restic-rest-htpasswd.age";
|
||||
mode = "0400";
|
||||
owner = "restic";
|
||||
};
|
||||
@@ -264,7 +264,7 @@ in
|
||||
(mkIf cfg.server.storage.enable {
|
||||
age.secrets = {
|
||||
"hdd-luks-crypt-key" = {
|
||||
file = "${mysecrets}/hdd-luks-crypt-key.age";
|
||||
file = "${mysecrets}/server/hdd-luks-crypt-key.age";
|
||||
mode = "0400";
|
||||
owner = "root";
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user