feat(secrets): add work gitconfig secret and a no-sudo replace flow (#346)

* feat(secrets): add work gitconfig secret, symlink it in home-manager

* feat(secrets): add a no-sudo replace recipe for new secrets

* refactor(secrets): move work gitconfig symlink into the desktop layer
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-02 13:51:17 +08:00
1 parent 9c44117f75
commit 0c8f0fe0c3
8 files changed
+84 -15

No files matched your search

+16 -9
View File
@@ -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 <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 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
Generated
+3 -3
View File
@@ -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"
+9
View File
@@ -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 ];
+13
View File
@@ -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";
}
+6 -1
View File
@@ -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:
+11 -2
View File
@@ -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
+10
View File
@@ -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
+16
View File
@@ -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;
};
};
})