c90d368432
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.
155 lines
7.0 KiB
Nix
155 lines
7.0 KiB
Nix
{ 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
|
||
'';
|
||
}
|
||
])
|
||
]
|
||
);
|
||
};
|
||
}
|