From 2b9e446876074d4660534b122cd0b785218ae492 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 16 Jul 2026 13:50:15 +0200 Subject: [PATCH] Integrate with yubikey --- README.org | 81 ++++++++++++++++++++++++++ machines/fujin/main/default.nix | 2 + modules/default.nix | 1 + modules/yubikey/default.nix | 100 ++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 modules/yubikey/default.nix diff --git a/README.org b/README.org index f20b653..f7e5553 100644 --- a/README.org +++ b/README.org @@ -405,6 +405,87 @@ dov = { }; #+end_src +** YubiKey +Provides PAM U2F authentication (touch YubiKey for login/sudo) and a seamless SSH agent backed by the YubiKey PIV applet. + +#+begin_src nix +dov = { + yubikey.enable = true; +}; +#+end_src + +*** PAM U2F (login & sudo) +After enabling the module and rebuilding, enroll your YubiKey: + +1. Create the YubiKey config directory: + #+begin_src sh + mkdir -p ~/.config/Yubico + #+end_src + +2. Generate the U2F key mapping (touch YubiKey when prompted): + #+begin_src bash + nix-shell -p pam_u2f --run 'pamu2fcfg -u fujin' > ~/.config/Yubico/u2f_keys + #+end_src + + *Nushell users:* use ~out>~ instead of ~>~: + #+begin_src + nix-shell -p pam_u2f --run 'pamu2fcfg -u fujin' out> ~/.config/Yubico/u2f_keys + #+end_src + +3. Add a backup YubiKey (optional): + #+begin_src bash + nix-shell -p pam_u2f --run 'pamu2fcfg -u fujin -n' >> ~/.config/Yubico/u2f_keys + #+end_src + +4. Rebuild and switch: + #+begin_src sh + sudo nixos-rebuild switch --flake .#fujin + #+end_src + +5. Test: + #+begin_src sh + sudo true # should prompt to touch YubiKey + #+end_src + +The default mode is =sufficient= (touch key *or* type password). If the key is absent or not enrolled, the normal password prompt follows — you cannot lock yourself out. + +*** SSH with FIDO2-backed keys +The module enables the standard OpenSSH agent (not GPG agent's SSH emulation, which doesn't reliably support =-sk= keys). The YubiKey uses FIDO2 — the same interface that PAM U2F uses — so no PIV/pcscd setup is needed for SSH. + +1. Generate a FIDO2-backed SSH key (one-time, touch YubiKey when prompted): + #+begin_src sh + ssh-keygen -t ed25519-sk -C "yubikey@fujin" + #+end_src + + *Resident key* (recoverable on a new machine without the stub file): + #+begin_src sh + ssh-keygen -t ed25519-sk -O resident -C "yubikey@fujin" + #+end_src + +2. Load the key into the agent (once per login session, enter passphrase): + #+begin_src sh + ssh-add ~/.ssh/id_ed25519_sk + #+end_src + + *After a rebuild*, log out and back in so the shell picks up the new =SSH_AUTH_SOCK=. In an existing session: + #+begin_src sh + export SSH_AUTH_SOCK=/run/user/$(id -u)/ssh-agent + #+end_src + +3. Copy the public key to remote hosts: + #+begin_src sh + ssh-copy-id -i ~/.ssh/id_ed25519_sk.pub susano + #+end_src + +4. Test: + #+begin_src sh + ssh susano # touch YubiKey, no password + #+end_src + +Each SSH authentication requires a physical YubiKey touch. The passphrase is only asked when loading the key into the agent (step 2), not per-connection. + +*Note:* The key stub (~/.ssh/id_ed25519_sk) is tied to this specific generation — regenerating produces a different key. Back it up or use =-O resident=. + * Notes and Configuration Details ** Remote Build Configuration To leverage remote builds (e.g., building fujin configurations on izanagi), you need to set up SSH keys for the root user: diff --git a/machines/fujin/main/default.nix b/machines/fujin/main/default.nix index a94e430..5b8348c 100644 --- a/machines/fujin/main/default.nix +++ b/machines/fujin/main/default.nix @@ -121,6 +121,8 @@ display-manager.ly.enable = true; gaming.enable = true; + + yubikey.enable = true; }; ### diff --git a/modules/default.nix b/modules/default.nix index 39e19c4..0ddf505 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -15,5 +15,6 @@ ./gitlab ./jenkins ./gaming + ./yubikey ]; } diff --git a/modules/yubikey/default.nix b/modules/yubikey/default.nix new file mode 100644 index 0000000..9cd3f8a --- /dev/null +++ b/modules/yubikey/default.nix @@ -0,0 +1,100 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; + +let + cfg = config.dov.yubikey; +in { + options.dov.yubikey = { + enable = mkEnableOption "YubiKey integration (PAM U2F + SSH agent)"; + + pamControl = mkOption { + type = types.enum [ "sufficient" "required" ]; + default = "sufficient"; + description = '' + PAM control flag for U2F authentication. + - "sufficient": touch YubiKey OR type password (default). + Can't lock yourself out — if the key is missing or not + enrolled, the normal password prompt follows. + - "required": touch YubiKey AND type password (true 2FA). + ''; + }; + + pamServices = mkOption { + type = types.listOf types.str; + default = [ "login" "sudo" ]; + description = '' + PAM services to enable U2F for. Scoped per-service rather + than globally so that e.g. sshd is not affected. + ''; + }; + + authFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Central U2F authfile path. When null, uses the default + per-user location (~/.config/Yubico/u2f_keys). Set to a + nix-store path for a central, non-user-writable mapping. + + Generate mappings with: + pamu2fcfg -u <username> + ''; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + # --- Shared infrastructure: smart card daemon + device access --- + # hardware.gpgSmartcards installs the CCID udev rules that give + # pcscd permission to open the YubiKey's smart-card USB interface. + hardware.gpgSmartcards.enable = true; + services.pcscd.enable = true; + services.udev.packages = with pkgs; [ + yubikey-personalization + libfido2 + ]; + + environment.systemPackages = with pkgs; [ + yubikey-manager # `ykman` CLI + pam_u2f # `pamu2fcfg` for key enrollment + libfido2 # `fido2-token` management + ]; + } + + # --- PAM U2F: touch YubiKey for login / sudo --- + { + security.pam.u2f = { + enable = true; + control = cfg.pamControl; + settings = { + cue = true; # "Please touch the device." + interactive = true; # "Insert your U2F device, then press ENTER." + nouserok = true; # fall through to password if key not enrolled yet + } // optionalAttrs (cfg.authFile != null) { + inherit (cfg) authFile; + }; + }; + + # Enable U2F per-service, not globally (avoids enabling for sshd). + security.pam.services = genAttrs cfg.pamServices (_: { + u2fAuth = true; + }); + } + + # --- SSH agent for FIDO2 (-sk) keys --- + # yubikey-agent is incompatible with YubiKey firmware 5.7.x (PIV + # management key auth changed). GPG agent's SSH emulation doesn't + # reliably handle -sk keys. Use the standard OpenSSH agent instead, + # which is the same software stack that created the key. + { + programs.gnupg.agent.enableSSHSupport = mkForce false; + programs.ssh.startAgent = true; + } + ]); +}