Files
chiasson-nix/modules/ssh/home-manager/default.nix
T
Olivier c90d368432 refactor(ssh): restructure inventory and modularize helper logic
Relocate SSH inventory and rbw socket wiring from `modules/lib/` to
dedicated `modules/ssh/` modules to improve logical grouping.

- Move `ssh-inventory.nix` to `modules/ssh/inventory.nix`.
- Move `rbw-ssh-socket.nix` to `modules/ssh/rbw.nix`.
- Relocate `users-merge.nix` logic into `modules/system/users.nix` to
  reduce dependency on generic library modules.
- Update `docs/conventions.md` to reflect new module paths.
- Refactor `modules/deploy/navi.nix` to use a local helper for
  generating Navi hive configurations.
- Add Pi5 specific Niri KDL configuration snippets.
2026-07-15 23:27:03 -03:00

155 lines
7.0 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/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/ssh/inventory.nix`.";
}
{
assertion = missingExtraKeys == [ ];
message = "ssh.outbound.rbw: extraIdentities has entries with no publicKey in `modules/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
'';
}
])
]
);
};
}