Files
chiasson-nix/modules/ssh/home-manager/default.nix
T
Olivier db88a44435 refactor(ssh): centralize identity file logic and optimize host selection
Refactor the SSH configuration generation to reduce duplication and
improve efficiency.

- Extract `IdentityFile` line generation into a shared variable in
  `ssh-inventory.nix` to ensure consistency between per-host and
  gitea blocks.
- Optimize `selectedHosts` resolution in `home-manager/default.nix`
  by reusing `activeHosts` when the "all" wildcard is used.
- Tighten IPv4 regex matching in `ssh-inventory.nix` using anchors.
- Update `users-merge.nix` to propagate `extraIdentities` to the
  rbw outbound configuration.
- Set default `authorizedHosts` to "all" for inbound SSH users.
2026-07-15 18:20:40 -03:00

149 lines
6.8 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{ 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;
# Resolve to the actual host attrs for the template. The "all" case
# reuses `activeHosts` directly instead of rebuilding the same attrset.
selectedHosts =
if cfg.hosts == [ "all" ] then
inventory.activeHosts
else
builtins.listToAttrs (map (n: { name = n; value = inventory.hosts.${n}; }) 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_<user>.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_<account>.pub` filter file and a
# `Match user <account>` block in `~/.ssh/config` so
# `ssh <account>@<host>` 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 {
inherit selectedHosts;
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
'';
}
])
]);
};
}