From 1bb084339d41971f042885d7ea46008af7ef5fa7 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sat, 10 Oct 2026 13:37:21 -0700 Subject: [PATCH] Set up Yubikey --- nix/modules/home/agents.nix | 9 +++++---- nix/modules/home/hosts/wisteria.nix | 21 +++++++++++++++++++++ nix/modules/home/packages.nix | 8 ++++++++ nix/modules/home/programs.nix | 21 +++++++++++++++++++++ nix/nixos/hosts/wisteria/default.nix | 10 +++++++++- 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/nix/modules/home/agents.nix b/nix/modules/home/agents.nix index 12efcd7..cdcce95 100755 --- a/nix/modules/home/agents.nix +++ b/nix/modules/home/agents.nix @@ -698,16 +698,17 @@ in # Extensions/skills as pinned npm specs (@version): `pi update` leaves # pinned specs alone, so versions move only when this file does. Pi fetches # tarballs into ~/.pi/agent/npm at runtime — pinned, but not Nix-hermetic. + # MCP is built in since pi 0.99 (servers in ~/.pi/agent/mcp.json, `pi mcp + # add`); pi-mcp-adapter would shadow its /mcp command, so it is not listed. packages = [ "npm:@juicesharp/rpiv-advisor@1.20.0" # second opinion from a stronger model "npm:@juicesharp/rpiv-todo@1.20.0" # task-list overlay, survives compaction "npm:pi-claude-style-tools@1.0.64" # Claude Code-style tool rows + Shiki diffs - "npm:pi-intercom@0.6.0" # live subagent supervision + grouped results + "npm:pi-intercom@0.16.1" # live subagent supervision + grouped results "npm:pi-kanban@1.0.0" # kanban web dashboard over rpiv-todo + subagents - "npm:pi-lens@3.8.63" # LSP, linters, type-checking - "npm:pi-mcp-adapter@2.10.0" # MCP bridge (Pi has none native) + "npm:pi-lens@4.3.0" # LSP, linters, type-checking "npm:pi-prompt-template-model@0.10.0" # prompt templates w/ model/skill frontmatter - "npm:pi-subagents@0.32.0" # subagent delegation + "npm:pi-subagents@0.76.1" # subagent delegation "npm:pi-web-access@0.13.0" # web search + fetch + gh clone + PDF "npm:pi-zentui@0.3.0" # Starship footer + Opencode-style editor ]; diff --git a/nix/modules/home/hosts/wisteria.nix b/nix/modules/home/hosts/wisteria.nix index d3b2e9a..aad234f 100755 --- a/nix/modules/home/hosts/wisteria.nix +++ b/nix/modules/home/hosts/wisteria.nix @@ -3,6 +3,12 @@ { config, pkgs, lib, ... }: let + nanoSigningKeyPath = "${config.home.homeDirectory}/.ssh/id_signing_nano"; + sshSignNoAgent = pkgs.writeShellScriptBin "ssh-sign-no-agent" '' + unset SSH_AUTH_SOCK + exec ${pkgs.openssh}/bin/ssh-keygen "$@" + ''; + # Mute key: toggle the sink volume between 0 and its previous level rather # than flipping PipeWire's mute flag (`wpctl set-mute`), because some sinks # (notably AirPods over the HFP profile) don't honour it. Force the level to @@ -259,6 +265,21 @@ in { }; programs = { + # Sign with the FIDO2 key on the Nano, by path to the key handle. The + # signer runs without SSH_AUTH_SOCK: gcr-ssh-agent auto-loads key pairs + # from ~/.ssh, and an agent cannot prompt for the PIN that + # verify-required needs ("agent refused operation"). Without the agent, + # ssh-keygen talks to the YubiKey itself and asks on the tty or through + # SSH_ASKPASS. + git = { + signing.key = lib.mkForce nanoSigningKeyPath; + settings.gpg.ssh.program = "${sshSignNoAgent}/bin/ssh-sign-no-agent"; + }; + jujutsu.settings.signing = { + key = lib.mkForce nanoSigningKeyPath; + backends.ssh.program = "${sshSignNoAgent}/bin/ssh-sign-no-agent"; + }; + # Include the wallpaper-derived palette, written by matugen # (niri-session/theming.nix). extraConfig is emitted last in kitty.conf, # so it wins. diff --git a/nix/modules/home/packages.nix b/nix/modules/home/packages.nix index 23c88c3..aa95a17 100755 --- a/nix/modules/home/packages.nix +++ b/nix/modules/home/packages.nix @@ -72,7 +72,15 @@ let # a rogue extension cannot poison the sources the host's own nix will later # build from. Requires unprivileged overlayfs (kernel >= 5.11); a kernel # without it fails loudly at launch rather than silently degrading. + # + # No display sockets are bound (/run/user is absent), so WAYLAND_DISPLAY + # and DISPLAY are unset to match. Pi then sees a headless host and copies + # via OSC 52 through the pty, which kitty writes to the clipboard. That + # is write-only: nothing in here can read the clipboard (e.g. a password + # copied from KeePassXC). exec ${pkgs.bubblewrap}/bin/bwrap \ + --unsetenv WAYLAND_DISPLAY \ + --unsetenv DISPLAY \ --ro-bind /nix/store /nix/store \ --ro-bind /run/current-system /run/current-system \ --ro-bind /etc /etc \ diff --git a/nix/modules/home/programs.nix b/nix/modules/home/programs.nix index 4b93c37..1483195 100755 --- a/nix/modules/home/programs.nix +++ b/nix/modules/home/programs.nix @@ -11,8 +11,13 @@ let }; }; + # Software key, held in KeePassXC and served by the agent. Hosts with a + # YubiKey override the signing key (see hosts/wisteria.nix). signingKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM5RGNvxkIOd7lbaCUIe4m2fOZeO0tlTvJXzMZZdtBfo hello@brooklynzelenka.com"; + # FIDO2 signing key on the 5C Nano (resident, PIN + touch per signature). + nanoSigningKey = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIKqXGI9y/f14Tlj8Q4WhCq+r8W8wtr0s0KzZdA4MO0ezAAAAC3NzaDpzaWduaW5n hello@brooklynzelenka.com (nano signing)"; + # 1Password-managed SSH public keys, pinned per host to avoid MaxAuthTries sshKeys = { bedrock = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOmJy3W56uqJjXGCHYOSJkLw+Ae/SgtF8B0qtjcDxtXp bedrock"; @@ -88,6 +93,16 @@ in { theme = "catppuccin-mocha"; editor = "${pkgs.vim}/bin/vim"; }; + + # Sign on push rather than on every snapshot, so a PIN-and-touch key + # prompts once per push instead of constantly. + signing = { + backend = "ssh"; + behavior = "drop"; + key = signingKey; + backends.ssh.allowed-signers = "~/.ssh/allowed_signers"; + }; + git.sign-on-push = true; }; }; @@ -250,6 +265,12 @@ in { }; # 1Password SSH public keys for host-pinned identity selection + # Every key that has signed commits as this email; old keys stay listed so + # past signatures keep verifying. + home.file.".ssh/allowed_signers".text = lib.concatMapStrings + (key: "hello@brooklynzelenka.com namespaces=\"git\" ${key}\n") + [ signingKey nanoSigningKey ]; + home.file.".ssh/1p_bedrock.pub".text = sshKeys.bedrock; home.file.".ssh/1p_codeberg.pub".text = sshKeys.codeberg; home.file.".ssh/1p_kielo.pub".text = sshKeys.kielo; diff --git a/nix/nixos/hosts/wisteria/default.nix b/nix/nixos/hosts/wisteria/default.nix index 57d820a..636d73e 100755 --- a/nix/nixos/hosts/wisteria/default.nix +++ b/nix/nixos/hosts/wisteria/default.nix @@ -1,4 +1,4 @@ -# System configuration for wisteria (Framework Laptop 13, Ryzen AI 300, 32 GB) +# System configuration for wisteria (Framework Laptop 13, Ryzen AI 300, 64 GB) { config, lib, pkgs, hostname, nixos-hardware, ... }: { imports = [ ./hardware.nix @@ -165,6 +165,13 @@ # Printing services.printing.enable = true; + # YubiKeys: pcscd for the smart-card (CCID) side that ykman uses for OATH + # and PIV, and udev rules so the logged-in user can program the OTP slots + # (challenge-response for KeePassXC) without sudo. FIDO2 (ssh -sk keys, + # passkeys) already works through systemd's uaccess rules. + services.pcscd.enable = true; + services.udev.packages = [ pkgs.yubikey-personalization ]; + # Audio security.rtkit.enable = true; services.pipewire = { @@ -222,6 +229,7 @@ # SIGSEGV loop — a grey window that never paints. Must be on $PATH, not just # in the niri config. pkgs.xwayland-satellite + pkgs.yubikey-manager ]; # Services -- 2.51.2