aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-06-19 16:36:01 +0100
committerLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-06-19 16:36:01 +0100
commit7eacd3c160f23fbff65c510aae70266b33b48bc2 (patch)
treea5fc4f00c4e55b9a211ad7e315aae816c159c271
parentf484c7be7e72b18b337c57e6427bc4eaed5b3d13 (diff)
downloaddotfiles-7eacd3c160f23fbff65c510aae70266b33b48bc2.tar.gz
dotfiles-7eacd3c160f23fbff65c510aae70266b33b48bc2.tar.bz2
dotfiles-7eacd3c160f23fbff65c510aae70266b33b48bc2.zip
Use local gpg-agent on VM
-rw-r--r--dot_config/git/config3
-rw-r--r--dot_config/zsh/dot_zprofile31
-rw-r--r--dot_config/zsh/dot_zshrc43
-rw-r--r--nix/README.md55
-rw-r--r--nix/vm.nix14
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
diff --git a/nix/vm.nix b/nix/vm.nix
index fe3cdcb..44b5b5f 100644
--- a/nix/vm.nix
+++ b/nix/vm.nix
@@ -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