{ lib, ... }: { flake.lib.sshInventory = let # Hosts: pure network endpoints. No public keys live here anymore — # keys are per USER (see `users` below), one key per person. hosts = { "14900k" = { hostName = "192.168.2.25"; aliases = [ "14900k" "nixdesk" ]; }; ideapad = { hostName = "192.168.2.229"; aliases = [ "ideapad" ]; }; t2mbp = { hostName = "192.168.2.15"; aliases = [ "t2mbp" ]; }; "uConsole" = { hostName = "192.168.2.99"; aliases = [ "uConsole" "uconsole" ]; }; # Scratchpad / recovery target — hostName isn't an IP, so `activeHosts` # excludes it from navi deployments. Leave it in `hosts` for inventory # visibility while recovery is in progress. test = { hostName = "test"; aliases = [ "test" ]; }; "nix-server" = { hostName = "192.168.2.238"; aliases = [ "nix-server" ]; }; r5500 = { hostName = "192.168.2.100"; aliases = [ "r5500" ]; }; }; # Per-user public keys. One keypair per person, generated once with # `ssh-keygen -t ed25519 -C ` (or `rbw gen ssh-key `), # private key stored in Bitwarden under the same name. Paste the # `ssh-ed25519 AAAA… ` line here. users = { olivier = { publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICpORzDw8kEOobjywl7bQaEp6Tez1c/ZP+7VBDsTcM8I olivier"; }; server = { publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICsksvNGOt/ebJtpRi9ocLBjwyl16VSaf+BsL2Hz5PRy server"; }; builder = { publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIILBtfW1cq9mG6uAjAfpZuVa1EWDSTPSCThapDXXlLgV builder"; }; }; # Hosts SSH is actually safe to deploy/scan to: anything whose hostName # parses as an IPv4 dotted-quad. Filters out scratchpad/non-routable # entries (e.g. `hostName = "test"`). activeHosts = lib.filterAttrs (_: h: lib.match "[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+" h.hostName != null) hosts; # .pub filename convention. OpenSSH 7.3+ accepts a `.pub` file as # IdentityFile to filter `IdentityAgent` (rbw) keys without storing the # private key on disk — keep this trick. mkIdentityFileName = userName: ".ssh/id_ed25519_${lib.strings.toLower userName}.pub"; mkSshConfigTemplate = { selectedHosts ? activeHosts, user ? null, # Catalog accounts the operator may authenticate AS via # `ssh @`. Each entry gets a `Match user ` # block that overrides IdentityFile so OpenSSH asks the rbw agent # for that account's private key. `cfg.user` is already covered # by the Host blocks, so don't list it here. extraIdentities ? [ ], identityAgent ? "SSH_AUTH_SOCK", }: let # Match blocks for cross-account SSH. Filter out cfg.user since # the matching pub is already loaded via the Host block's # IdentityFile line — re-emitting it would just duplicate. matchBlocks = lib.concatStringsSep "\n" (map (n: '' Match user ${n} IdentityFile ~/${mkIdentityFileName n} IdentitiesOnly yes '') (builtins.filter (n: n != user) extraIdentities)); # Per-host block. `IdentityFile` (when `user` is set) is a .pub file # pointing at the agent's matching private key. With IdentitiesOnly # yes, only agent keys matching that pub are tried — fixes the old # "too many auth failures" symptom by design. hostBlock = hostName: let e = selectedHosts.${hostName}; hostPatterns = lib.concatStringsSep " " (e.aliases ++ [ e.hostName ]); idLine = if user == null then "" else " IdentityFile ~/${mkIdentityFileName user}\n"; in '' Host ${hostPatterns} HostName ${e.hostName} ${idLine} IdentityAgent ${identityAgent} IdentitiesOnly yes ''; # Gitea block: always emitted. `User git`, port 222. olivier's # public key (passed via `IdentityFile`) is what gitea accepts # because it gets pushed into gitea's authorized_keys through # gitea's own UI for that account. giteaBlock = '' Host git.chiasson.cloud gitea HostName 192.168.2.238 Port 222 User git ${if user == null then "" else " IdentityFile ~/${mkIdentityFileName user}\n"} IdentityAgent ${identityAgent} IdentitiesOnly yes ''; hostBlocks = lib.concatStringsSep "\n" (map hostBlock (lib.attrNames selectedHosts)); in # Filter empty sections so an absent Match block (no extraIdentities) # doesn't add leading whitespace to the rendered config. lib.concatStringsSep "\n" (lib.filter (s: s != "") [ matchBlocks giteaBlock hostBlocks '' Host * IdentitiesOnly yes IdentityAgent none '' ]); # Per-user line for `users.users..openssh.authorizedKeys.keys`. # `selection` is either "all" or a list of source identifiers for # sshd's `from=` constraint (IP, CIDR, or hostname if reverse-DNS works). # Returns null if inventory is missing the user's pubkey — caller is # expected to assert and fail the build rather than silently lock out. mkAuthorizedKeyLine = userName: selection: let entry = users.${userName} or null; in if entry == null then null else if (entry.publicKey or "") == "" then null else if selection == "all" then entry.publicKey else ''from="${lib.concatStringsSep "," selection}" ${entry.publicKey}''; in { inherit hosts users activeHosts; mkSshConfigTemplate = mkSshConfigTemplate; mkAuthorizedKeyLine = mkAuthorizedKeyLine; mkIdentityFileName = mkIdentityFileName; }; }