feat(keys): SSH + GPG agent via gpg-agent, pinentry-qt

Add modules/home/keys.nix (nomarchy.keys.enable, default on): one agent —
services.gpg-agent with enableSshSupport fronts SSH, so a single pinentry
handles every passphrase (GPG signing/decryption and SSH key unlocks).
pinentry-qt for a reliable native-Wayland prompt under Hyprland, themed by
Stylix's Qt config. Cache TTLs 30 min / 2 h.

SSH_AUTH_SOCK is exported by the agent's zsh integration (terminal git/ssh
is the supported path). gnome-keyring stays the Secret Service — modern
versions run no SSH agent, so there's no socket contention; screen lock
doesn't flush the agent cache.

nix flake check passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-13 20:50:45 +01:00
parent 05d995f8e0
commit 3757e7d66f
5 changed files with 55 additions and 8 deletions

View File

@@ -264,7 +264,8 @@
"Bash(xargs cat)", "Bash(xargs cat)",
"Bash(nix-instantiate --parse modules/home/rofi.nix)", "Bash(nix-instantiate --parse modules/home/rofi.nix)",
"Bash(nix-instantiate --parse modules/home/shell.nix)", "Bash(nix-instantiate --parse modules/home/shell.nix)",
"Bash(command -v nix)" "Bash(command -v nix)",
"Bash(nix-instantiate --parse modules/home/keys.nix)"
] ]
} }
} }

View File

@@ -350,13 +350,15 @@ close · `SUPER+1..9` workspaces · `Print` region screenshot.
also locks hyprlock, so the user types a password twice. Lock only on also locks hyprlock, so the user types a password twice. Lock only on
suspend (not hibernate), or skip the hyprlock prompt when resuming from suspend (not hibernate), or skip the hyprlock prompt when resuming from
a LUKS-encrypted hibernate — see `modules/home/idle.nix`. a LUKS-encrypted hibernate — see `modules/home/idle.nix`.
- **Key agents & pinentry:** no SSH/GPG agent ships yet, so signing/auth - **Key agents & pinentry:** ships `modules/home/keys.nix`
prompts have nowhere to go. Wire up an agent + a Wayland-native graphical (`nomarchy.keys.enable`): one agent — `services.gpg-agent` with
pinentry: `services.ssh-agent` (or gpg-agent's `enableSshSupport`), `enableSshSupport` fronts SSH, so a single `pinentry-qt` (native-Wayland,
`programs.gnupg.agent` with `pinentry-gnome3`/`pinentry-qt`, `SSH_AUTH_SOCK` Stylix-themed Qt) handles GPG and SSH passphrases alike. `SSH_AUTH_SOCK`
exported, and the pinentry styled from the palette where it can be. Decide comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
one-agent (gpg-agent fronting SSH) vs. two, and confirm it cooperates with re-prompting. gnome-keyring stays the Secret Service (modern versions run
hyprlock/hypridle on unlock. no SSH agent, so no socket contention); screen lock doesn't flush the
cache. Remaining (optional): a session-level `SSH_AUTH_SOCK` export so GUI
clients launched outside a shell also see the agent.
## Known issues & follow-ups ## Known issues & follow-ups
-**Yazi TOML parse error on startup:** yazi 26.x made fetchers' -**Yazi TOML parse error on startup:** yazi 26.x made fetchers'

View File

@@ -18,6 +18,7 @@
./yazi.nix # flagship TUI file manager, themed + plugins ./yazi.nix # flagship TUI file manager, themed + plugins
./osd.nix # swayosd volume/brightness OSD, themed ./osd.nix # swayosd volume/brightness OSD, themed
./shell.nix # zsh + starship + bat/eza/zoxide, themed ./shell.nix # zsh + starship + bat/eza/zoxide, themed
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
]; ];
# Clipboard history (wl-paste watcher); browsed via the SUPER+CTRL+V # Clipboard history (wl-paste watcher); browsed via the SUPER+CTRL+V

42
modules/home/keys.nix Normal file
View File

@@ -0,0 +1,42 @@
# SSH + GPG agent — one agent for everything.
#
# gpg-agent fronts SSH (enableSshSupport), so a single pinentry handles
# every passphrase: GPG signing/decryption and SSH key unlocks alike. SSH
# keys are added with `ssh-add` (gpg-agent stores them by keygrip).
#
# Pinentry is pinentry-qt — reliable native-Wayland under Hyprland (no
# GNOME session to lean on) and themed by Stylix's Qt config, so the
# passphrase dialog tracks the palette.
#
# SSH_AUTH_SOCK is exported by the agent's shell integration (zsh, on via
# home.shell.enableZshIntegration in shell.nix). Terminal git/ssh is the
# supported path; a GUI client launched outside a shell won't inherit the
# socket — revisit with a session-level export if that's ever wanted.
#
# gnome-keyring (system side) stays the Secret Service for application
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
# SSH_AUTH_SOCK contention with gpg-agent. Screen lock (hyprlock/hypridle)
# doesn't flush the agent cache — unlocked keys persist across a lock, as
# expected; the cache TTLs below govern re-prompting instead.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
in
{
config = lib.mkIf cfg.keys.enable {
programs.gpg.enable = true;
services.gpg-agent = {
enable = true;
enableSshSupport = true;
pinentry.package = pkgs.pinentry-qt;
# Cache unlocked keys for a working session, then re-prompt.
defaultCacheTtl = 1800; # 30 min since last use
maxCacheTtl = 7200; # 2 h hard cap
defaultCacheTtlSsh = 1800;
maxCacheTtlSsh = 7200;
};
};
}

View File

@@ -69,6 +69,7 @@
yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; }; yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; };
osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; }; osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; };
shell.enable = lib.mkEnableOption "the zsh shell experience (starship prompt, bat/eza/zoxide)" // { default = true; }; shell.enable = lib.mkEnableOption "the zsh shell experience (starship prompt, bat/eza/zoxide)" // { default = true; };
keys.enable = lib.mkEnableOption "the SSH + GPG agent (gpg-agent fronting SSH, pinentry-qt)" // { default = true; };
ghostty.enable = lib.mkEnableOption "Nomarchy's Ghostty configuration" // { default = true; }; ghostty.enable = lib.mkEnableOption "Nomarchy's Ghostty configuration" // { default = true; };
btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; }; btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; };
stylix.enable = lib.mkEnableOption "Stylix theming for the long tail of apps (GTK, Qt, cursors)" // { default = true; }; stylix.enable = lib.mkEnableOption "Stylix theming for the long tail of apps (GTK, Qt, cursors)" // { default = true; };