{ self, ... }: { flake.homeManagerModules.sshOutboundRbw = { config, lib, pkgs, ... }: let cfg = config.chiasson.ssh.outbound.rbw; inventory = self.lib.sshInventory; # Resolve which hosts to emit blocks for, and which user identity to # use as the IdentityFile filter against the rbw agent. selectedHostNames = if cfg.hosts == [ "all" ] then builtins.attrNames inventory.activeHosts else cfg.hosts; missingHosts = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames; # All identities this HM user may authenticate AS: cfg.user ∪ # extraIdentities. `cfg.user` can be null (e.g. user not in inventory # yet) — in that case the Host-block IdentityFile line is suppressed # and cross-account SSH is wired from extraIdentities alone. effectiveIdentities = lib.filter (n: n != null) ([ cfg.user ] ++ cfg.extraIdentities); defaultUserHasKey = cfg.user == null || (inventory.users.${cfg.user}.publicKey or "") != ""; # Each extra identity must have a pubkey in the inventory. Dropping # it silently would produce a dead Match block — fail the build. missingExtraKeys = lib.filter (n: (inventory.users.${n}.publicKey or "") == "") cfg.extraIdentities; in { options.chiasson.ssh.outbound.rbw = { enable = lib.mkEnableOption "Generated `~/.ssh/config` + rbw agent socket."; user = lib.mkOption { type = lib.types.nullOr lib.types.str; default = config.home.username; description = '' Inventory user whose public key is written to `~/.ssh/id_ed25519_.pub` and used as `IdentityFile` to filter the rbw agent. Set `null` to disable the per-user .pub file / IdentityFile line (e.g. when the user isn't in the inventory yet). ''; }; hosts = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "all" ]; description = "Inventory hosts to emit (or `[ \"all\" ]`)."; }; manageSshConfig = lib.mkOption { type = lib.types.bool; default = true; description = "Write `~/.ssh/config` (disable for manual management)."; }; # Cross-account SSH on the operator's laptop/desktop. Each entry # adds a `~/.ssh/id_ed25519_.pub` filter file and a # `Match user ` block in `~/.ssh/config` so # `ssh @` picks the right key from the rbw agent. # `cfg.user` is already wired by the Host blocks — don't list it here. extraIdentities = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "server" "builder" ]; description = '' Catalog accounts this HM user may authenticate AS, in addition to `cfg.user`. Each entry must have a publicKey pasted into `modules/lib/ssh-inventory.nix`. Typical for the operator's primary laptop: `[ "server" "builder" ]` so fleet/ops commands like `ssh server@r5500` or `ssh builder@nix-server` work without touching `/etc/passwd` on each host. ''; }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { assertions = [ { assertion = missingHosts == [ ]; message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missingHosts}"; } { assertion = defaultUserHasKey; message = "ssh.outbound.rbw: no publicKey for inventory user `${cfg.user}` in `modules/lib/ssh-inventory.nix`."; } { assertion = missingExtraKeys == [ ]; message = "ssh.outbound.rbw: extraIdentities has entries with no publicKey in `modules/lib/ssh-inventory.nix`: ${builtins.concatStringsSep ", " missingExtraKeys}"; } # `enable=true` with neither a default user nor any extras gives the # agent no IdentityFile at all; with IdentitiesOnly yes the user # would silently be unable to authenticate as any catalog user. { assertion = effectiveIdentities != [ ]; message = "ssh.outbound.rbw: enabled but no identities wired. Set `chiasson.ssh.outbound.rbw.user` (defaults to `home.username`) or list catalog accounts under `extraIdentities`."; } ]; } (lib.mkMerge [ { home.packages = with pkgs; [ rbw pinentry-gtk2 ]; home.sessionVariables.SSH_AUTH_SOCK = self.lib.rbwSshSocket.sessionVariable; # Write a `.pub` file per identity this HM user may act as # (cfg.user ∪ extraIdentities). OpenSSH reads each one via # IdentityFile/Match to filter agent keys — they never hold # private key material, so 0644 (HM's default) is fine. # OpenSSH's StrictModes only rejects group/other-writable files. home.file = lib.listToAttrs (map (n: { name = inventory.mkIdentityFileName n; value.text = "${inventory.users.${n}.publicKey}\n"; }) effectiveIdentities); programs.ssh.enable = lib.mkIf cfg.manageSshConfig false; } (lib.mkIf cfg.manageSshConfig { home.file.".ssh/config".text = inventory.mkSshConfigTemplate { selectedHosts = builtins.listToAttrs (map (n: { name = n; value = inventory.hosts.${n}; }) selectedHostNames); user = cfg.user; inherit (cfg) extraIdentities; }; }) { systemd.user.services.rbw-agent-bootstrap = { Unit = { Description = "Bootstrap rbw SSH agent"; PartOf = [ "graphical-session.target" ]; After = [ "graphical-session.target" ]; }; Service = { Type = "oneshot"; ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'"; RemainAfterExit = true; }; Install.WantedBy = [ "graphical-session.target" ]; }; home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' ${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-gtk2}/bin/pinentry-gtk-2" >/dev/null 2>&1 || true ''; } ]) ]); }; }