mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
refactor(secrets): move nushell-secrets.nu out of agenix (#423)
This commit is contained in:
1 parent
92740ec968
commit
0e12440148
10 files changed
+76
-38
No files matched your search
@@ -36,6 +36,7 @@ bump, and the commands that destroy rollback points are in the `nix-config-updat
|
|||||||
| remote host | anything on a Colmena host | `ssh root@<host> journalctl -b -p err`, same commands over SSH |
|
| remote host | anything on a Colmena host | `ssh root@<host> journalctl -b -p err`, same commands over SSH |
|
||||||
| MicroVM guest | guest down or unreachable | on the VM host: `systemctl status microvm@<guest>` and `microvm-tap-interfaces@<guest>`; then `br0` |
|
| MicroVM guest | guest down or unreachable | on the VM host: `systemctl status microvm@<guest>` and `microvm-tap-interfaces@<guest>`; then `br0` |
|
||||||
| secrets | missing or unreadable `/etc/agenix/*` | the `nix-config-secrets` skill |
|
| secrets | missing or unreadable `/etc/agenix/*` | the `nix-config-secrets` skill |
|
||||||
|
| shell | nushell exits 1 with `File not found` | whether `~/.secrets/` exists on the host; the `nix-config-secrets` skill |
|
||||||
|
|
||||||
## 2. Read the evaluated value
|
## 2. Read the evaluated value
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ in [hosts/README.md](../../../hosts/README.md).
|
|||||||
the configuration name; Niri desktops append `-niri`, because `just niri` deploys
|
the configuration name; Niri desktops append `-niri`, because `just niri` deploys
|
||||||
`$(hostname)-niri`. The `hostname` eval test encodes this.
|
`$(hostname)-niri`. The `hostname` eval test encodes this.
|
||||||
2. **Secrets come from another repository.** The new host decrypts nothing until its host key is a
|
2. **Secrets come from another repository.** The new host decrypts nothing until its host key is a
|
||||||
recipient in `nix-secrets`; see the `nix-config-secrets` skill.
|
recipient in `nix-secrets`; see the `nix-config-secrets` skill. A new desktop also needs its
|
||||||
|
synced dotfiles restored before the first switch, or the shell starts without them.
|
||||||
3. **The shared policy modules are not optional.** Several eval tests assert a policy for every
|
3. **The shared policy modules are not optional.** Several eval tests assert a policy for every
|
||||||
configuration, so an under-wired host fails `just test` instead of failing in production.
|
configuration, so an under-wired host fails `just test` instead of failing in production.
|
||||||
4. **Build before you install.** `just test`, `just eval-host <name>`, and `just build-host <name>`
|
4. **Build before you install.** `just test`, `just eval-host <name>`, and `just build-host <name>`
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: nix-config-secrets
|
name: nix-config-secrets
|
||||||
description:
|
description:
|
||||||
Use when adding, changing, renaming, or removing an agenix secret, wiring one into a host, or
|
Use when adding, changing, renaming, or removing an agenix secret, deciding whether a secret
|
||||||
fixing a decryption or activation failure in this repo.
|
belongs to agenix or to the encrypted dotfiles sync, wiring a secret into a host, or fixing a
|
||||||
|
decryption or activation failure in this repo.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Working with secrets
|
# Working with secrets
|
||||||
@@ -14,10 +15,35 @@ in this public repository.
|
|||||||
|
|
||||||
Read [secrets/README.md](../../../secrets/README.md) for the concepts and the recipient rule.
|
Read [secrets/README.md](../../../secrets/README.md) for the concepts and the recipient rule.
|
||||||
|
|
||||||
|
## Which flow a file belongs to
|
||||||
|
|
||||||
|
Two age flows live in `~/codes/nix-secrets`, and a file belongs to exactly one.
|
||||||
|
|
||||||
|
| Flow | Shape | Consumed as | `flake.lock` bump |
|
||||||
|
| ------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------- | ----------------- |
|
||||||
|
| **agenix** (`secrets.nix`) | static and one-way: repository → host, decrypted at activation | `/run/agenix/<name>` or an `environment.etc` copy | yes |
|
||||||
|
| **dotfiles sync** (`dotfiles/`) | changes on the host; `save`/`restore` are manual, outside Nix | a real file under `$HOME`, edited by hand or by its tool | **no** |
|
||||||
|
|
||||||
|
agenix is for the secrets that barely change and that a Nix module or service reads from the
|
||||||
|
decrypted path. Everything else is a synced dotfile: Nix does not read it, it changes often, or it
|
||||||
|
is a config file its tool keeps rewriting that happens to carry a few secrets inside. The pin
|
||||||
|
decides it, not the encryption: `flake.lock` fixes the agenix revision, so every edit costs a
|
||||||
|
rebuild and switch on every desktop.
|
||||||
|
|
||||||
|
Nothing in this flake evaluates the sync flow: no `secrets.nix` entry, no `agenix -r`, no lock bump,
|
||||||
|
so the three-step order in §2 does not apply to it. Its manifest and file list live in
|
||||||
|
`~/codes/nix-secrets/dotfiles/`; this repository carries the pointer, never the inventory. Do not
|
||||||
|
list synced paths here or copy one into a Nix module.
|
||||||
|
|
||||||
|
`nushell-secrets.nu` already moved to `~/.secrets/nushell-secrets.nu`. It has no agenix entry and
|
||||||
|
must not get one back.
|
||||||
|
|
||||||
## Core rules
|
## Core rules
|
||||||
|
|
||||||
1. **Never read a decrypted secret.** Reference its path; do not `cat`, copy, or print it. Prove a
|
1. **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.
|
change with metadata (mode, owner, timestamps), never with content. The same holds for a file the
|
||||||
|
sync flow owns: it is a live credential under `$HOME`, and its path is listed in the private
|
||||||
|
repository, not here.
|
||||||
2. **Three steps, in order.** The private repository changes first, then this repository's
|
2. **Three steps, in order.** The private repository changes first, then this repository's
|
||||||
`flake.lock` moves `mysecrets` to that commit, then the declaration and consumer change here.
|
`flake.lock` moves `mysecrets` to that commit, then the declaration and consumer change here.
|
||||||
`file = "${mysecrets}/x.age"` resolves against the locked revision, so a declaration that lands
|
`file = "${mysecrets}/x.age"` resolves against the locked revision, so a declaration that lands
|
||||||
@@ -43,6 +69,7 @@ Read [secrets/README.md](../../../secrets/README.md) for the concepts and the re
|
|||||||
| Which host gets which group | `modules.secrets.<group>.enable` in `outputs/<system>/src/<name>.nix` or a host module |
|
| 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>`) |
|
| 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 |
|
| Decryption key | `age.identityPaths`: the host's SSH host key; `/persistent/etc/ssh/...` on a preservation host |
|
||||||
|
| Secret-bearing dotfiles edited in place | `~/codes/nix-secrets/dotfiles/` — private, not a flake input, no entry in `secrets.nix` |
|
||||||
|
|
||||||
The private repository groups the `.age` files: `desktop/` (only desktops decrypt them), `server/`
|
The private repository groups the `.age` files: `desktop/` (only desktops decrypt them), `server/`
|
||||||
(any server), `certs/`, and `public/`.
|
(any server), `certs/`, and `public/`.
|
||||||
@@ -124,9 +151,16 @@ you set, an old copy gone) instead of assuming activation did it.
|
|||||||
Check `secrets/`, `home/`, `modules/`, `hosts/`, outputs, tests, and the private repository.
|
Check `secrets/`, `home/`, `modules/`, `hosts/`, outputs, tests, and the private repository.
|
||||||
|
|
||||||
- Delete the `age.secrets` entry, its `environment.etc` placement, and every consumer in one change;
|
- Delete the `age.secrets` entry, its `environment.etc` placement, and every consumer in one change;
|
||||||
a declaration whose file no longer exists breaks activation.
|
a declaration whose file no longer exists breaks activation. On darwin also drop the path from the
|
||||||
|
`chown`/`chmod` list in `system.activationScripts.postActivation`.
|
||||||
- Remove it from `secrets.nix` and delete the file in the private repository, then bump the lock.
|
- 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.
|
- A rename changes the attribute name and every consumer. The `.age` filename is separate.
|
||||||
|
- Moving a secret **out** of agenix into the sync flow is that deletion plus what agenix used to
|
||||||
|
guarantee: the consumer points at a `$HOME` path, and on a preservation host the directory joins
|
||||||
|
`preservation.preserveAt`, which also sets its mode. A sourced file that may be missing is left to
|
||||||
|
fail: nushell has no conditional `source`
|
||||||
|
([nushell#8214](https://github.com/nushell/nushell/issues/8214)), and a host without the file is
|
||||||
|
misconfigured, not half-working.
|
||||||
|
|
||||||
## 7. Failure modes
|
## 7. Failure modes
|
||||||
|
|
||||||
@@ -140,6 +174,12 @@ you set, an old copy gone) instead of assuming activation did it.
|
|||||||
`/persistent/etc/ssh/...` path, which exists before preservation mounts `/etc`.
|
`/persistent/etc/ssh/...` path, which exists before preservation mounts `/etc`.
|
||||||
- **The first macOS activation fails** on the `/etc/agenix` copies, because they run before
|
- **The first macOS activation fails** on the `/etc/agenix` copies, because they run before
|
||||||
`activate-agenix` has decrypted anything. Run it again.
|
`activate-agenix` has decrypted anything. Run it again.
|
||||||
|
- **Nushell exits 1 with `File not found` and the shell has no aliases:** the sourced secret file is
|
||||||
|
missing. agenix guaranteed the path; the sync flow does not, and the failure is left loud on
|
||||||
|
purpose. Restore the file on that host. Never silence it with an empty placeholder: the sync tool
|
||||||
|
reads that as a local edit and reports a conflict.
|
||||||
|
- **A secret is gone after a reboot on a desktop:** it moved out of agenix and its `$HOME` directory
|
||||||
|
is not in `preservation.preserveAt`, so the tmpfs root drops it.
|
||||||
|
|
||||||
## Why these rules exist
|
## Why these rules exist
|
||||||
|
|
||||||
@@ -151,3 +191,5 @@ you set, an old copy gone) instead of assuming activation did it.
|
|||||||
host had; the reference moved to `modules/nixos/desktop/nix.nix` (core rule 4).
|
host had; the reference moved to `modules/nixos/desktop/nix.nix` (core rule 4).
|
||||||
- `260da1ee chore: rename the nushell secret, and forbid reading decrypted secrets` - the no-reading
|
- `260da1ee chore: rename the nushell secret, and forbid reading decrypted secrets` - the no-reading
|
||||||
rule.
|
rule.
|
||||||
|
- The move of `nushell-secrets.nu` out of agenix - the `flake.lock` pin, not the encryption, made
|
||||||
|
each edit of a hand-edited secret cost a rebuild and switch on every desktop.
|
||||||
+2
-1
@@ -131,7 +131,8 @@ For an impactful change, follow these steps in order, scaled to its risk:
|
|||||||
dependencies; ask before creating a flake or installing another way.
|
dependencies; ask before creating a flake or installing another way.
|
||||||
- The user's interactive shell is **nushell**; bash is only started from within nushell,
|
- The user's interactive shell is **nushell**; bash is only started from within nushell,
|
||||||
occasionally, for tasks nushell can't do. Put shell env, aliases, and per-session secrets in the
|
occasionally, for tasks nushell can't do. Put shell env, aliases, and per-session secrets in the
|
||||||
Nushell config (or `nushell-secrets.nu`), not in `bashrc`/`zshrc`.
|
Nushell config, not in `bashrc`/`zshrc`. The secret block is `~/.secrets/nushell-secrets.nu`:
|
||||||
|
hand-edited, sourced as code by the shell, not Nix-managed, and agents MUST NOT read or edit it.
|
||||||
- Use `gh` for authorized GitHub operations; keep SSH for GitHub Git remotes and make sure the
|
- Use `gh` for authorized GitHub operations; keep SSH for GitHub Git remotes and make sure the
|
||||||
Nix-managed `~/.ssh/config` is used; rerun outside the sandbox if it is rejected.
|
Nix-managed `~/.ssh/config` is used; rerun outside the sandbox if it is rejected.
|
||||||
- Code layout: `~/codes` = personal, `~/work` = work code, `~/src/<repo>` = source checkouts.
|
- Code layout: `~/codes` = personal, `~/work` = work code, `~/src/<repo>` = source checkouts.
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
config,
|
||||||
nu_scripts,
|
nu_scripts,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
programs.nushell = {
|
programs.nushell = {
|
||||||
# load the alias file for work
|
|
||||||
# the file must exist, otherwise nushell will complain about it!
|
# the file must exist, otherwise nushell will complain about it!
|
||||||
#
|
#
|
||||||
# currently, nushell does not support conditional sourcing of files
|
# currently, nushell does not support conditional sourcing of files
|
||||||
# https://github.com/nushell/nushell/issues/8214
|
# https://github.com/nushell/nushell/issues/8214
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
# decrypted secret: AI agents must not read it
|
# decrypted secret: AI agents must not read it
|
||||||
source /etc/agenix/nushell-secrets.nu
|
source ${config.home.homeDirectory}/.secrets/nushell-secrets.nu
|
||||||
|
|
||||||
# Directories in this constant are searched by the
|
# Directories in this constant are searched by the
|
||||||
# `use` and `source` commands.
|
# `use` and `source` commands.
|
||||||
|
|||||||
@@ -260,6 +260,11 @@ in
|
|||||||
directory = ".gnupg";
|
directory = ".gnupg";
|
||||||
mode = "0700";
|
mode = "0700";
|
||||||
}
|
}
|
||||||
|
# hand-edited secret config; Nix does not deploy it
|
||||||
|
{
|
||||||
|
directory = ".secrets";
|
||||||
|
mode = "0700";
|
||||||
|
}
|
||||||
{
|
{
|
||||||
directory = ".ssh";
|
directory = ".ssh";
|
||||||
mode = "0700";
|
mode = "0700";
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@
|
|||||||
// cost context here and nowhere else.
|
// cost context here and nowhere else.
|
||||||
//
|
//
|
||||||
// mcp-grafana talks to the homelab Grafana. The service-account token stays in
|
// mcp-grafana talks to the homelab Grafana. The service-account token stays in
|
||||||
// the environment (GRAFANA_SERVICE_ACCOUNT_TOKEN, set by nushell-secrets.nu),
|
// the environment (GRAFANA_SERVICE_ACCOUNT_TOKEN, exported by the hand-edited
|
||||||
// never in git.
|
// ~/.secrets/nushell-secrets.nu), never in git.
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"servers": {
|
"servers": {
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ This directory contains this `README.md`, the `Justfile` with the agenix recipes
|
|||||||
`nixos.nix`/`darwin.nix` files that decrypt all my secrets via `agenix`. Then, I can use them in
|
`nixos.nix`/`darwin.nix` files that decrypt all my secrets via `agenix`. Then, I can use them in
|
||||||
this flake.
|
this flake.
|
||||||
|
|
||||||
|
## Two Flows, and Which One a File Belongs To
|
||||||
|
|
||||||
|
agenix holds the secrets that barely change and that a Nix module or service reads from the
|
||||||
|
decrypted path. `flake.lock` pins `mysecrets`, so an agenix secret is a frozen revision: each edit
|
||||||
|
costs a lock bump plus a rebuild and switch on every host that reads it.
|
||||||
|
|
||||||
|
Anything else is a synced dotfile: Nix does not read it, it changes often, or it is a config file
|
||||||
|
its tool keeps rewriting that happens to carry a few secrets inside. The private repository syncs
|
||||||
|
those as age ciphertext under `dotfiles/`, by hand and outside Nix, so there is no `secrets.nix`
|
||||||
|
entry and no lock bump. `nushell-secrets.nu` moved that way and is `~/.secrets/nushell-secrets.nu`
|
||||||
|
now; do not re-add it to `age.secrets`. The file list stays in the private repository.
|
||||||
|
|
||||||
## Which Keys Go on a Secret
|
## Which Keys Go on a Secret
|
||||||
|
|
||||||
**Every secret is decryptable by the desktops and by the offline `recovery_key`, and those two have
|
**Every secret is decryptable by the desktops and by the offline `recovery_key`, and those two have
|
||||||
@@ -229,6 +241,10 @@ the `age.secrets.<name>.path` argument, which defaults to `/run/agenix/<name>`.
|
|||||||
3. On the new host:
|
3. On the new host:
|
||||||
1. Clone this repo and run `nixos-rebuild switch` to deploy it, all the secrets will be decrypted
|
1. Clone this repo and run `nixos-rebuild switch` to deploy it, all the secrets will be decrypted
|
||||||
automatically via the host private key.
|
automatically via the host private key.
|
||||||
|
2. A desktop also needs the synced dotfiles: regenerate their recipient list from `desktop_keys`
|
||||||
|
and run the sync's `restore` there before the first switch, so the shell's secret block is
|
||||||
|
present when the config sources it. See
|
||||||
|
[Two Flows](#two-flows-and-which-one-a-file-belongs-to).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -77,12 +77,6 @@
|
|||||||
}
|
}
|
||||||
// user_readable;
|
// user_readable;
|
||||||
|
|
||||||
# decrypted secret: AI agents must not read it, only reference the path
|
|
||||||
"nushell-secrets.nu" = {
|
|
||||||
file = "${mysecrets}/desktop/nushell-secrets.nu.age";
|
|
||||||
}
|
|
||||||
// user_readable;
|
|
||||||
|
|
||||||
# work git identity, symlinked to ~/work/.gitconfig by home-manager
|
# work git identity, symlinked to ~/work/.gitconfig by home-manager
|
||||||
"work-gitconfig" = {
|
"work-gitconfig" = {
|
||||||
file = "${mysecrets}/desktop/work-gitconfig.age";
|
file = "${mysecrets}/desktop/work-gitconfig.age";
|
||||||
@@ -108,10 +102,6 @@
|
|||||||
# The following secrets are used by home-manager modules
|
# The following secrets are used by home-manager modules
|
||||||
# But nix-darwin doesn't support environment.etc.<name>.mode
|
# But nix-darwin doesn't support environment.etc.<name>.mode
|
||||||
# So we need to change its mode manually
|
# So we need to change its mode manually
|
||||||
"agenix/nushell-secrets.nu" = {
|
|
||||||
source = config.age.secrets."nushell-secrets.nu".path;
|
|
||||||
};
|
|
||||||
|
|
||||||
"agenix/work-gitconfig" = {
|
"agenix/work-gitconfig" = {
|
||||||
source = config.age.secrets."work-gitconfig".path;
|
source = config.age.secrets."work-gitconfig".path;
|
||||||
};
|
};
|
||||||
@@ -124,11 +114,9 @@
|
|||||||
if [ -d /etc/agenix ]; then
|
if [ -d /etc/agenix ]; then
|
||||||
${pkgs.coreutils}/bin/chown ${myvars.username} \
|
${pkgs.coreutils}/bin/chown ${myvars.username} \
|
||||||
/etc/agenix/ssh-key-romantic \
|
/etc/agenix/ssh-key-romantic \
|
||||||
/etc/agenix/nushell-secrets.nu \
|
|
||||||
/etc/agenix/work-gitconfig
|
/etc/agenix/work-gitconfig
|
||||||
${pkgs.coreutils}/bin/chmod 0500 \
|
${pkgs.coreutils}/bin/chmod 0500 \
|
||||||
/etc/agenix/ssh-key-romantic \
|
/etc/agenix/ssh-key-romantic \
|
||||||
/etc/agenix/nushell-secrets.nu \
|
|
||||||
/etc/agenix/work-gitconfig
|
/etc/agenix/work-gitconfig
|
||||||
|
|
||||||
${pkgs.coreutils}/bin/chown root /etc/agenix/rclone.conf
|
${pkgs.coreutils}/bin/chown root /etc/agenix/rclone.conf
|
||||||
|
|||||||
@@ -115,12 +115,6 @@ in
|
|||||||
}
|
}
|
||||||
// user_readable;
|
// user_readable;
|
||||||
|
|
||||||
# decrypted secret: AI agents must not read it, only reference the path
|
|
||||||
"nushell-secrets.nu" = {
|
|
||||||
file = "${mysecrets}/desktop/nushell-secrets.nu.age";
|
|
||||||
}
|
|
||||||
// user_readable;
|
|
||||||
|
|
||||||
# work git identity, symlinked to ~/work/.gitconfig by home-manager
|
# work git identity, symlinked to ~/work/.gitconfig by home-manager
|
||||||
"work-gitconfig" = {
|
"work-gitconfig" = {
|
||||||
file = "${mysecrets}/desktop/work-gitconfig.age";
|
file = "${mysecrets}/desktop/work-gitconfig.age";
|
||||||
@@ -145,16 +139,6 @@ in
|
|||||||
mode = "0000";
|
mode = "0000";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Used by a home-manager module, so it must be readable by the user.
|
|
||||||
# Setting `mode` makes environment.etc copy the file instead of
|
|
||||||
# symlinking the runtime secret, so the owner must be set as well —
|
|
||||||
# otherwise it becomes a world-readable, root-owned copy of the secret.
|
|
||||||
"agenix/nushell-secrets.nu" = {
|
|
||||||
source = config.age.secrets."nushell-secrets.nu".path;
|
|
||||||
mode = "0400";
|
|
||||||
user = myvars.username;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Used by a home-manager module, so it must be readable by the user.
|
# Used by a home-manager module, so it must be readable by the user.
|
||||||
# Setting `mode` makes environment.etc copy the file instead of
|
# Setting `mode` makes environment.etc copy the file instead of
|
||||||
# symlinking the runtime secret, so the owner must be set as well —
|
# symlinking the runtime secret, so the owner must be set as well —
|
||||||
|
|||||||
Reference in new issue
Block a user