diff options
| author | 2026-06-19 16:36:01 +0100 | |
|---|---|---|
| committer | 2026-06-19 16:36:01 +0100 | |
| commit | 7eacd3c160f23fbff65c510aae70266b33b48bc2 (patch) | |
| tree | a5fc4f00c4e55b9a211ad7e315aae816c159c271 | |
| parent | f484c7be7e72b18b337c57e6427bc4eaed5b3d13 (diff) | |
| download | dotfiles-7eacd3c160f23fbff65c510aae70266b33b48bc2.tar.gz dotfiles-7eacd3c160f23fbff65c510aae70266b33b48bc2.tar.bz2 dotfiles-7eacd3c160f23fbff65c510aae70266b33b48bc2.zip | |
Use local gpg-agent on VM
| -rw-r--r-- | dot_config/git/config | 3 | ||||
| -rw-r--r-- | dot_config/zsh/dot_zprofile | 31 | ||||
| -rw-r--r-- | dot_config/zsh/dot_zshrc | 43 | ||||
| -rw-r--r-- | nix/README.md | 55 | ||||
| -rw-r--r-- | nix/vm.nix | 14 |
5 files changed, 46 insertions, 100 deletions
diff --git a/dot_config/git/config b/dot_config/git/config index 3874410..6416efc 100644 --- a/dot_config/git/config +++ b/dot_config/git/config @@ -153,6 +153,5 @@ [credential "smtp://127.0.0.1:1025"] helper = "!f() { test \"$1\" = get && printf 'password=%s\\n' \"$(pass show proton/bridge-smtp)\"; }; f" [include] - ; Machine-local overrides (e.g. SSH-format signing on the remote-dev VM). - ; Git silently skips this if the file is absent. + ; Machine-local overrides. Git silently skips this if the file is absent. path = ~/.config/git/config.local diff --git a/dot_config/zsh/dot_zprofile b/dot_config/zsh/dot_zprofile index 20852db..9150382 100644 --- a/dot_config/zsh/dot_zprofile +++ b/dot_config/zsh/dot_zprofile @@ -75,33 +75,10 @@ export LESS="-F --RAW-CONTROL-CHARS" # ── GPG / SSH ───────────────────────────────────────────────────────────────── unset SSH_AGENT_PID -# Forwarded ssh-agent sockets live at /tmp/ssh-XXX/agent.NNN — a path -# that disappears the moment the originating ssh connection drops, -# leaving any long-running zellij pane (and its children: claude, -# nvim, etc.) pointing at a dead socket. Keep a stable -# ~/.ssh/agent.sock symlink that we re-aim on every login, and export -# the stable path so processes inherit a value that survives -# reconnects. Reattaching a zellij session after `ssh` → signing / -# git-fetch keep working without any per-pane re-export. -if [[ -n "$SSH_CONNECTION" && -S "$SSH_AUTH_SOCK" ]]; then - stable_sock="$HOME/.ssh/agent.sock" - # Only retarget if the current symlink target is dead. Sshd unlinks - # the per-connection socket file on disconnect, so [[ -S ]] on the - # resolved path is a reliable liveness probe. Avoiding gratuitous - # retargets keeps multi-connection setups stable: the first - # connection seeds the symlink, subsequent logins keep using it, - # and only if that connection drops does the next login retarget. - current_target="$(readlink "$stable_sock" 2>/dev/null)" - if [[ ! -S "$current_target" ]]; then - ln -sfn "$SSH_AUTH_SOCK" "$stable_sock" - fi - export SSH_AUTH_SOCK="$stable_sock" - unset stable_sock current_target -else - # Local login: route ssh auth through gpg-agent. - SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" - export SSH_AUTH_SOCK -fi +# Always route SSH auth through the machine-local gpg-agent. The VM imports its +# own work GPG key; we deliberately do not use forwarded ssh-agent sockets. +SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" +export SSH_AUTH_SOCK # ── FZF ─────────────────────────────────────────────────────────────────────── export FZF_DEFAULT_COMMAND="fd --type file --follow --hidden --exclude .git --color=always" diff --git a/dot_config/zsh/dot_zshrc b/dot_config/zsh/dot_zshrc index d78c9e5..113955c 100644 --- a/dot_config/zsh/dot_zshrc +++ b/dot_config/zsh/dot_zshrc @@ -407,41 +407,6 @@ reload-env() { done < "/proc/$pid/environ" } -# Refresh the ssh-agent socket inside a zellij pane that has outlived -# its originating SSH connection. zprofile keeps ~/.ssh/agent.sock -# aimed at the live forwarded socket on every reconnect, so the stable -# path is current — this just re-exports it for shells whose own -# SSH_AUTH_SOCK still holds the dead per-connection path captured -# when zellij was first started. Already-running children -# (claude-code, etc.) must still be restarted: env is inherited, not -# observed. -ssh-agent-refresh() { - local stable="$HOME/.ssh/agent.sock" - local current sock - current="$(readlink "$stable" 2>/dev/null)" - # Healthy path: existing target still responsive. - if [[ -S "$current" ]] && SSH_AUTH_SOCK="$current" ssh-add -l >/dev/null 2>&1; then - export SSH_AUTH_SOCK="$stable" - print -r -- "ssh-agent: live → $current" - return 0 - fi - # Symlink dead — scan all forwarded sockets from any concurrent ssh - # session and retarget to the first one that responds to ssh-add. - # Handles the case where the connection that originally seeded the - # symlink has dropped but another session is still alive. - for sock in /tmp/ssh-*/agent.*(N); do - [[ -S $sock ]] || continue - if SSH_AUTH_SOCK="$sock" ssh-add -l >/dev/null 2>&1; then - ln -sfn "$sock" "$stable" - export SSH_AUTH_SOCK="$stable" - print -r -- "ssh-agent: re-pointed → $sock" - return 0 - fi - done - print -r -- "ssh-agent-refresh: no live forwarded agent found; reconnect over ssh with -A first" >&2 - return 1 -} - # Just alias j='just' alias dj='just --justfile ~/dotfiles/justfile --working-directory ~/dotfiles' @@ -489,6 +454,14 @@ _dot_compdef lsd l=lsd la=lsd lt=lsd unfunction _dot_compdef # ── GPG agent ───────────────────────────────────────────────────────────────── +# Interactive shells can outlive the login environment that spawned them +# (notably inside zellij). If they inherited an old forwarded-agent socket, +# switch back to the machine-local gpg-agent SSH socket. +if [[ -z "$SSH_AUTH_SOCK" || "$SSH_AUTH_SOCK" == /tmp/ssh-* || "$SSH_AUTH_SOCK" == "$HOME/.ssh/agent.sock" ]]; then + SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" + export SSH_AUTH_SOCK +fi + # Set GPG_TTY to this shell's actual TTY (not the login console) and tell # the agent so pinentry prompts appear in the right terminal export GPG_TTY=$TTY diff --git a/nix/README.md b/nix/README.md index 2bf3383..4a27ae1 100644 --- a/nix/README.md +++ b/nix/README.md @@ -87,49 +87,31 @@ If a project needs a newer build toolchain, drop a `flake.nix` + `.envrc` in that project tree (direnv + nix-direnv is already wired up). Don't add it to `common.nix`/`host.nix`/`vm.nix`. -## Commit signing on the VM (SSH-format, no GPG secrets) +## Commit signing and SSH auth on the VM (GPG) -GPG private keys never leave the host. Commits on the VM are signed -with the **forwarded SSH agent** in SSH-signature format, using the -authentication subkey gpg-agent already exposes via `ssh-add -L`. +The VM uses its own local `gpg-agent`, like the host. Import the work +GPG private key manually on the VM; do not use SSH agent forwarding for +commit signing or SSH auth. One-time setup on the VM: ```sh -mkdir -p ~/.config/git - -# allowed_signers: maps your committer email to the SSH pubkey of the -# auth subkey. Adjust the grep if you have multiple keys. -printf '%s %s\n' \ - "$(git config user.email)" \ - "$(ssh-add -L | head -n1)" \ - > ~/.config/git/allowed_signers - -# Machine-local git override (NOT tracked in dotfiles). -cat > ~/.config/git/config.local <<EOF -[gpg] - format = ssh -[gpg "ssh"] - allowedSignersFile = ~/.config/git/allowed_signers -[user] - signingkey = $(ssh-add -L | head -n1 | awk '{print $1" "$2}') -EOF +rm -f ~/.ssh/agent.sock ~/.config/git/allowed_signers +gpg --import /path/to/work-private-key.asc +gpg --edit-key 3298945F717C85F8 trust quit +gpg --list-secret-keys --with-keygrip 3298945F717C85F8 ``` -The tracked `dot_config/git/config` ends with `[include] path = -~/.config/git/config.local`, so the override is picked up -automatically (and silently ignored on machines that don't have it). - -Required on the **host's** `~/.ssh/config` for the VM `Host` block: - -``` -ForwardAgent yes -``` +Add the authentication subkey keygrip to `~/.gnupg/sshcontrol`. The +tracked git config already uses normal OpenPGP signing, so no +`~/.config/git/config.local` override is needed for SSH-format signing. +If `~/.config/git/config.local` only contains the old SSH-format +signing override, remove it too. -Verify on the VM after SSH-ing in: +Verify on the VM: ```sh -ssh-add -L # should list your auth pubkey(s) +ssh-add -L git commit --allow-empty -m test git log --show-signature -1 ``` @@ -137,9 +119,10 @@ git log --show-signature -1 ## Caveats - **GPG / pass**: HM installs `gnupg` and `pass` but does _not_ import - any private key. On the VM, use SSH-format signing via the forwarded - agent instead (see above). On the host, smartcard access via - `pcscd` is configured in `host.nix` (`~/.gnupg/scdaemon.conf`). + any private key. On the VM, import the work key manually and add the + authentication subkey keygrip to `~/.gnupg/sshcontrol`. On the host, + smartcard access via `pcscd` is configured in `host.nix` + (`~/.gnupg/scdaemon.conf`). - **Disk usage**: Nix store + nvim plugins consumes ~3-5 GB. Check partition size first on the VM. - **Network for first nvim launch**: `vim.pack.add` fetches plugins @@ -8,6 +8,11 @@ let dotfiles = "${builtins.getEnv "HOME"}/.local/share/dotfiles"; link = path: config.lib.file.mkOutOfStoreSymlink "${dotfiles}/${path}"; + vmGpgAgentConf = pkgs.writeText "gpg-agent.conf" '' + enable-ssh-support + pinentry-program ${pkgs.pinentry-curses}/bin/pinentry-curses + allow-loopback-pinentry + ''; in { imports = [ ./common.nix ]; @@ -125,6 +130,15 @@ in "${dotfiles}/private_dot_ssh/config" "$HOME/.ssh/config" ''; + # GnuPG needs strict file modes and a VM-local pinentry path. Private + # keys and sshcontrol stay machine-local; import/add the work key manually. + home.activation.gnupgConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + run install -d -m 700 "$HOME/.gnupg" + run install -m 600 \ + "${dotfiles}/private_dot_gnupg/gpg.conf" "$HOME/.gnupg/gpg.conf" + run install -m 600 "${vmGpgAgentConf}" "$HOME/.gnupg/gpg-agent.conf" + ''; + # ZDOTDIR redirect so login shells find ~/.config/zsh/.zprofile etc. # Also source HM's session-vars — HM normally drops these into # ~/.profile, but zsh login shells don't read .profile, and we don't |
