Commit 9bf3c5122dab

Vincent Demeester <vincent@sbr.pm>
2026-01-30 10:00:49
feat(ssh): improve FIDO2 key management for homelab hosts
- Add identityFile and identitiesOnly to sshConfig for all homelab hosts - Include .sbr.pm hosts alongside .home and .vpn in generated configs - Use lib.recursiveUpdate to properly merge critical infra overrides - Critical infra (athena, demeter, kerkouane) uses touch-required key - All other homelab hosts use no-touch homelab key Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 97b5937
Changed files (2)
home
common
lib
home/common/shell/openssh.nix
@@ -20,137 +20,138 @@ in
   programs.ssh = {
     enable = true;
     enableDefaultConfig = false;
-    matchBlocks = {
-      "*" = {
-        serverAliveInterval = 60;
-        hashKnownHosts = true;
-        userKnownHostsFile = "${config.home.homeDirectory}/.ssh/known_hosts";
-        addKeysToAgent = "confirm";
-        controlMaster = "auto";
-        controlPersist = "10m";
-        controlPath = "${config.home.homeDirectory}/.ssh/master-%C";
-      };
-      # Shpool session aliases (https://bower.sh/you-might-not-need-tmux)
-      # Usage: ssh <host>/<session-name>
-      # Example: ssh rhea.home/music, ssh aomi.home/dev
-    }
-    // (
-      # Generate shpool session aliases for each machine dynamically
+    matchBlocks =
       let
-        inherit (pkgs) lib;
-        mkShpoolAliases =
-          _: machine:
-          let
-            # Get hostname identifiers (e.g., "rhea.home", "rhea.vpn", "rhea.sbr.pm")
-            identifiers = builtins.filter (
-              x: (lib.hasSuffix ".home" x) || (lib.hasSuffix ".vpn" x) || (lib.hasSuffix ".sbr.pm" x)
-            ) (libx.sshHostIdentifier machine);
-            # For each identifier, create a Host block with /* wildcard
-            mkSessionBlock = id: {
-              name = "${id}/*";
-              value = {
-                hostname =
-                  if (lib.hasSuffix ".vpn" id) then
-                    builtins.head machine.net.vpn.ips
-                  else if (lib.hasSuffix ".home" id) then
-                    builtins.head machine.net.ips
-                  else
-                    id;
-                extraOptions = {
-                  RemoteCommand = "shpool-ssh-wrapper $(echo '%k' | cut -d/ -f2-)";
-                  # RemoteCommand = "bash -ic '[ -f ~/.local/share/kitty-ssh-kitten/zsh/kitty-integration ] && source ~/.local/share/kitty-ssh-kitten/zsh/kitty-integration 2>/dev/null; exec shpool-ssh-wrapper $(echo \"%k\" | cut -d/ -f2-)'";
-                  RequestTTY = "yes";
-                };
-              };
-            };
-          in
-          builtins.listToAttrs (map mkSessionBlock identifiers);
+        # Critical infra hosts that need touch-required key
+        criticalInfraOverrides = lib.optionalAttrs hasFido2Keys {
+          "athena.home".identityFile = "~/.ssh/id_critical_infra_sk";
+          "athena.vpn".identityFile = "~/.ssh/id_critical_infra_sk";
+          "athena.sbr.pm".identityFile = "~/.ssh/id_critical_infra_sk";
+          "demeter.home".identityFile = "~/.ssh/id_critical_infra_sk";
+          "demeter.vpn".identityFile = "~/.ssh/id_critical_infra_sk";
+          "demeter.sbr.pm".identityFile = "~/.ssh/id_critical_infra_sk";
+          "kerkouane.vpn".identityFile = "~/.ssh/id_critical_infra_sk";
+          "kerkouane.sbr.pm".identityFile = "~/.ssh/id_critical_infra_sk";
+        };
+        # Special case for aomi
+        aomiOverrides = lib.optionalAttrs isAomi {
+          "kerkouane.vpn" = {
+            identityFile = "~/.ssh/id_ed25519";
+            identitiesOnly = true;
+          };
+        };
       in
-      # Merge all shpool aliases for all machines
-      lib.attrsets.mergeAttrsList (lib.attrsets.mapAttrsToList mkShpoolAliases globals.machines)
-    )
-    // {
-      "github.com" = {
-        hostname = "github.com";
-        user = "git";
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_github_sk";
-        extraOptions = {
-          controlMaster = "auto";
-          controlPersist = "360";
-        };
-      };
-      "gitlab.com" = {
-        hostname = "gitlab.com";
-        user = "git";
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_gitlab_sk";
-        extraOptions = {
-          controlMaster = "auto";
-          controlPersist = "360";
-        };
-      };
-      "codeberg.org" = {
-        hostname = "codeberg.org";
-        user = "git";
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_codeberg_sk";
-        extraOptions = {
-          controlMaster = "auto";
-          controlPersist = "360";
-        };
-      };
-      "git.sr.ht" = {
-        hostname = "git.sr.ht";
-        user = "git";
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_srht_sk";
-        extraOptions = {
-          controlMaster = "auto";
-          controlPersist = "360";
-        };
-      };
-      "*.redhat.com" = {
-        user = "vdemeest";
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_redhat_sk";
-      };
-      "bootstrap.ospqa.com" = {
-        forwardAgent = true;
-      };
-      "192.168.1.*" = {
-        forwardAgent = true;
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
-        extraOptions = {
-          StrictHostKeyChecking = "no";
-          UserKnownHostsFile = "/dev/null";
-        };
-      };
-      "10.100.0.*" = {
-        forwardAgent = true;
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
-      };
-      # Critical infrastructure - uses id_critical_infra_sk (touch-required) by default
-      # For no-touch convenience, use: ssh -i ~/.ssh/id_homelab_sk <host>
-      "athena.home" = {
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_critical_infra_sk";
-      };
-      "athena.vpn" = {
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_critical_infra_sk";
-      };
-      "demeter.home" = {
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_critical_infra_sk";
-      };
-      "demeter.vpn" = {
-        identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_critical_infra_sk";
-      };
-      "kerkouane.vpn" = {
-        identityFile =
-          if hasFido2Keys then
-            "~/.ssh/id_critical_infra_sk"
-          else if isAomi then
-            "~/.ssh/id_ed25519"
-          else
-            null;
-        identitiesOnly = isAomi;
-      };
-    }
-    // libx.sshConfigs globals.machines;
+      lib.recursiveUpdate (
+        {
+          "*" = {
+            serverAliveInterval = 60;
+            hashKnownHosts = true;
+            userKnownHostsFile = "${config.home.homeDirectory}/.ssh/known_hosts";
+            addKeysToAgent = "confirm";
+            controlMaster = "auto";
+            controlPersist = "10m";
+            controlPath = "${config.home.homeDirectory}/.ssh/master-%C";
+          };
+          # Shpool session aliases (https://bower.sh/you-might-not-need-tmux)
+          # Usage: ssh <host>/<session-name>
+          # Example: ssh rhea.home/music, ssh aomi.home/dev
+        }
+        // (
+          # Generate shpool session aliases for each machine dynamically
+          let
+            inherit (pkgs) lib;
+            mkShpoolAliases =
+              _: machine:
+              let
+                # Get hostname identifiers (e.g., "rhea.home", "rhea.vpn", "rhea.sbr.pm")
+                identifiers = builtins.filter (
+                  x: (lib.hasSuffix ".home" x) || (lib.hasSuffix ".vpn" x) || (lib.hasSuffix ".sbr.pm" x)
+                ) (libx.sshHostIdentifier machine);
+                # For each identifier, create a Host block with /* wildcard
+                mkSessionBlock = id: {
+                  name = "${id}/*";
+                  value = {
+                    hostname =
+                      if (lib.hasSuffix ".vpn" id) then
+                        builtins.head machine.net.vpn.ips
+                      else if (lib.hasSuffix ".home" id) then
+                        builtins.head machine.net.ips
+                      else
+                        id;
+                    extraOptions = {
+                      RemoteCommand = "shpool-ssh-wrapper $(echo '%k' | cut -d/ -f2-)";
+                      # RemoteCommand = "bash -ic '[ -f ~/.local/share/kitty-ssh-kitten/zsh/kitty-integration ] && source ~/.local/share/kitty-ssh-kitten/zsh/kitty-integration 2>/dev/null; exec shpool-ssh-wrapper $(echo \"%k\" | cut -d/ -f2-)'";
+                      RequestTTY = "yes";
+                    };
+                  };
+                };
+              in
+              builtins.listToAttrs (map mkSessionBlock identifiers);
+          in
+          # Merge all shpool aliases for all machines
+          lib.attrsets.mergeAttrsList (lib.attrsets.mapAttrsToList mkShpoolAliases globals.machines)
+        )
+        # Generated configs for all machines (sets default id_homelab_sk)
+        // libx.sshConfigs globals.machines
+        # External hosts (new entries, not overrides)
+        // {
+          "github.com" = {
+            hostname = "github.com";
+            user = "git";
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_github_sk";
+            extraOptions = {
+              controlMaster = "auto";
+              controlPersist = "360";
+            };
+          };
+          "gitlab.com" = {
+            hostname = "gitlab.com";
+            user = "git";
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_gitlab_sk";
+            extraOptions = {
+              controlMaster = "auto";
+              controlPersist = "360";
+            };
+          };
+          "codeberg.org" = {
+            hostname = "codeberg.org";
+            user = "git";
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_codeberg_sk";
+            extraOptions = {
+              controlMaster = "auto";
+              controlPersist = "360";
+            };
+          };
+          "git.sr.ht" = {
+            hostname = "git.sr.ht";
+            user = "git";
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_srht_sk";
+            extraOptions = {
+              controlMaster = "auto";
+              controlPersist = "360";
+            };
+          };
+          "*.redhat.com" = {
+            user = "vdemeest";
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_redhat_sk";
+          };
+          "bootstrap.ospqa.com" = {
+            forwardAgent = true;
+          };
+          "192.168.1.*" = {
+            forwardAgent = true;
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
+            extraOptions = {
+              StrictHostKeyChecking = "no";
+              UserKnownHostsFile = "/dev/null";
+            };
+          };
+          "10.100.0.*" = {
+            forwardAgent = true;
+            identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
+          };
+        }
+      ) (lib.recursiveUpdate criticalInfraOverrides aomiOverrides);
     extraConfig = ''
       # IdentityAgent /run/user/1000/yubikey-agent/yubikey-agent.sock
       GlobalKnownHostsFile ~/.ssh/ssh_known_hosts ~/.ssh/ssh_known_hosts.redhat ~/.ssh/ssh_known_hosts.mutable
lib/functions.nix
@@ -153,8 +153,12 @@ let
               else if (lib.strings.hasSuffix ".home" x) then
                 builtins.head machine.net.ips
               else
+                # .sbr.pm uses the hostname directly (DNS resolution)
                 x;
             forwardAgent = false;
+            # Use FIDO2 homelab key for all homelab hosts
+            identityFile = "~/.ssh/id_homelab_sk";
+            identitiesOnly = true;
             # Disable IdentityAgent only for aomi.home (prevents yubikey prompts in TRAMP)
             extraOptions = lib.optionalAttrs (x == "aomi.home") {
               IdentityAgent = "none";
@@ -162,9 +166,12 @@ let
           };
         })
         (
-          builtins.filter (x: (lib.strings.hasSuffix ".home" x) || (lib.strings.hasSuffix ".vpn" x)) (
-            sshHostIdentifier machine
-          )
+          builtins.filter (
+            x:
+            (lib.strings.hasSuffix ".home" x)
+            || (lib.strings.hasSuffix ".vpn" x)
+            || (lib.strings.hasSuffix ".sbr.pm" x)
+          ) (sshHostIdentifier machine)
         )
     );