diff --git a/.agents/skills/nix-config-secrets/SKILL.md b/.agents/skills/nix-config-secrets/SKILL.md index 6d52ae75..a74e3ac8 100644 --- a/.agents/skills/nix-config-secrets/SKILL.md +++ b/.agents/skills/nix-config-secrets/SKILL.md @@ -50,14 +50,17 @@ The private repository groups the `.age` files: `desktop/` (only desktops decryp ## 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`](../../../secrets/Justfile) (they -handle key material and `$EDITOR`) and anything else under `sudo`. Commits and pushes follow the -global git rules. +non-interactive steps, bump the lock, and add the declaration and consumer. For a new secret, or a +full replacement, pipe the plaintext into the `replace` recipe: it removes the target first, so +agenix only encrypts and needs no `sudo`. Hand back only what needs a human: the partial `edit` and +the `rekey` recipes in [`secrets/Justfile`](../../../secrets/Justfile) (they decrypt the current +value, so they handle key material, `$EDITOR`, and `sudo`) and anything else under `sudo`. Commits +and pushes follow the global git rules. 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. + from core rule 3. Then encrypt it with the `replace` recipe (§3); ask the user only when the + plaintext has to come from an interactive session. 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 @@ -65,7 +68,8 @@ global git rules. 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 `, deploy, then verify as in §5. -Changing only a secret's value is steps 1, 2, and the deploy. +Changing only a secret's value is steps 1, 2, and the deploy: use `replace` for a full replacement +(the agent can run it) or `edit` for a partial change (needs the host key). ## 3. The private repository @@ -75,12 +79,15 @@ The agenix operations run against `~/codes/nix-secrets`. Their single source is `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 +just -f secrets/Justfile replace ./desktop/xxx.age < plaintext # add or fully replace (no sudo) +just -f secrets/Justfile edit ./desktop/xxx.age # partial edit (sudo, $EDITOR) +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. +`edit` and `rekey` decrypt the current value with the host key, so they need `sudo`; `replace` only +encrypts to the recipients in `secrets.nix`, so it needs no `sudo`. `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 diff --git a/flake.lock b/flake.lock index e21abdd4..a0a3e218 100644 --- a/flake.lock +++ b/flake.lock @@ -484,10 +484,10 @@ "mysecrets": { "flake": false, "locked": { - "lastModified": 1790915701, - "narHash": "sha256-ES+PUPSCJBE4pwhTBwgKsoBit3oXjrI/OJt5xxngeT4=", + "lastModified": 1790919428, + "narHash": "sha256-04+A6Hmjj2lXGanEWfqoQkkAGiQgVT0CMY5BTOlfcBo=", "ref": "refs/heads/main", - "rev": "37e3c3ee87ac649fc52f9314db1cdf672a3c62b0", + "rev": "7b78dbb128be0ca52ac2815d5d8a73c5f883dada", "shallow": true, "type": "git", "url": "ssh://git@github.com/ryan4yin/nix-secrets.git" diff --git a/home/base/core/git.nix b/home/base/core/git.nix index ad331bff..9941655b 100644 --- a/home/base/core/git.nix +++ b/home/base/core/git.nix @@ -3,6 +3,7 @@ lib, pkgs, myvars, + osConfig, ... }: { @@ -14,6 +15,14 @@ rm -f ${config.home.homeDirectory}/.gitconfig ''; + # Work identity (email/name) lives encrypted in nix-secrets and is symlinked + # to ~/work/.gitconfig, which `programs.git.includes` below makes git read for + # repos under ~/work. `work-gitconfig` is only declared on desktops, so the + # symlink follows the secret. + home.file."work/.gitconfig" = lib.mkIf ((osConfig.age.secrets or { }) ? "work-gitconfig") { + source = config.lib.file.mkOutOfStoreSymlink "/etc/agenix/work-gitconfig"; + }; + # GitHub CLI tool # https://cli.github.com/manual/ home.packages = [ pkgs.gh ]; diff --git a/home/base/gui/work-gitconfig.nix b/home/base/gui/work-gitconfig.nix new file mode 100644 index 00000000..1f96c100 --- /dev/null +++ b/home/base/gui/work-gitconfig.nix @@ -0,0 +1,13 @@ +{ config, ... }: +{ + # Work git identity (email/name), decrypted from nix-secrets. ~/work/.gitconfig + # is what `programs.git.includes` (home/base/core/git.nix) tells git to read + # for repos under ~/work. + # + # This lives in the desktop layer rather than core: base/gui is imported only + # by hosts that declare the desktop secret (the NixOS desktops via + # home/linux/gui.nix and macOS via home/darwin), so the consumer and the + # secret share the same gate. + home.file."work/.gitconfig".source = + config.lib.file.mkOutOfStoreSymlink "/etc/agenix/work-gitconfig"; +} diff --git a/secrets/Justfile b/secrets/Justfile index 26dfe1de..5317be68 100644 --- a/secrets/Justfile +++ b/secrets/Justfile @@ -13,10 +13,15 @@ editor := "hx" default: @just --list --justfile {{justfile()}} -# Edit or create a secret (interactive); pipe the plaintext to write non-interactively +# Partially edit an existing secret (interactive, needs sudo) edit file: sudo env EDITOR={{quote(editor)}} agenix -i {{identity}} -e {{quote(file)}} +# Create a secret, or fully replace one, from plaintext on stdin (no sudo) +replace file: + rm -f {{quote(file)}} + agenix -e {{quote(file)}} + # Re-encrypt every secret after adding or changing a recipient key [confirm("Re-encrypt every secret in ~/codes/nix-secrets?")] rekey: diff --git a/secrets/README.md b/secrets/README.md index 1415174a..aa4c01a8 100644 --- a/secrets/README.md +++ b/secrets/README.md @@ -107,8 +107,17 @@ in } ``` -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: +3. Create the secret file `xxx.age` from the repository root with the [`Justfile`](./Justfile) + `replace` recipe, piping the plaintext on stdin: + +```shell +just -f secrets/Justfile replace ./xxx.age < plaintext +``` + +`replace` deletes the target first, so agenix only encrypts to the public keys in `secrets.nix` and +never decrypts the old value — it needs no `sudo`. Use `edit` instead for a partial change to an +existing secret: it decrypts the current value first with the host key +(`/etc/ssh/ssh_host_ed25519_key`), so it needs `sudo`. ```shell just -f secrets/Justfile edit ./xxx.age diff --git a/secrets/darwin.nix b/secrets/darwin.nix index 507aabf5..683f81f2 100644 --- a/secrets/darwin.nix +++ b/secrets/darwin.nix @@ -82,6 +82,12 @@ file = "${mysecrets}/desktop/nushell-secrets.nu.age"; } // user_readable; + + # work git identity, symlinked to ~/work/.gitconfig by home-manager + "work-gitconfig" = { + file = "${mysecrets}/desktop/work-gitconfig.age"; + } + // user_readable; }; # place secrets in /etc/ @@ -105,6 +111,10 @@ "agenix/nushell-secrets.nu" = { source = config.age.secrets."nushell-secrets.nu".path; }; + + "agenix/work-gitconfig" = { + source = config.age.secrets."work-gitconfig".path; + }; }; # both the original file and the symlink should be readable and executable by the user diff --git a/secrets/nixos.nix b/secrets/nixos.nix index 7fc7b038..28249343 100644 --- a/secrets/nixos.nix +++ b/secrets/nixos.nix @@ -120,6 +120,12 @@ in file = "${mysecrets}/desktop/nushell-secrets.nu.age"; } // user_readable; + + # work git identity, symlinked to ~/work/.gitconfig by home-manager + "work-gitconfig" = { + file = "${mysecrets}/desktop/work-gitconfig.age"; + } + // user_readable; }; # place secrets in /etc/ @@ -148,6 +154,16 @@ in mode = "0400"; user = myvars.username; }; + + # 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/work-gitconfig" = { + source = config.age.secrets."work-gitconfig".path; + mode = "0400"; + user = myvars.username; + }; }; })