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.
340 lines
12 KiB
Nix
340 lines
12 KiB
Nix
# Users & SSH inbound: options + catalog defaults + integration. Single module
|
|
# that used to be split across `modules/system/users/{default,catalog-options,
|
|
# catalog-default,home-integration}.nix`. Per-host overrides / extra HM modules
|
|
# work the same way they always have; only the file structure changed.
|
|
{ self, ... }:
|
|
{
|
|
flake.nixosModules.users =
|
|
{ config, options, lib, ... }:
|
|
let
|
|
cfg = config.chiasson.users;
|
|
usersLib =
|
|
let
|
|
userHm = user: user.homeManager or { };
|
|
userSsh = user: user.ssh or { };
|
|
in
|
|
rec {
|
|
resolveHomeManagerModule =
|
|
moduleSpec:
|
|
let
|
|
t = builtins.typeOf moduleSpec;
|
|
in
|
|
if t == "path" || t == "string" then import moduleSpec else moduleSpec;
|
|
|
|
selectedUsersAttr =
|
|
{ catalog, enabled, hostOverrides }:
|
|
lib.listToAttrs (
|
|
map (name: {
|
|
inherit name;
|
|
value = lib.recursiveUpdate catalog.${name} (hostOverrides.${name} or { });
|
|
}) enabled
|
|
);
|
|
|
|
missingEnabledNames =
|
|
catalog: enabled: builtins.filter (name: !(builtins.hasAttr name catalog)) enabled;
|
|
|
|
strayHomeUserKeys =
|
|
homeUsers: enabled: builtins.filter (k: !(builtins.elem k enabled)) (builtins.attrNames homeUsers);
|
|
|
|
mkNixosUser =
|
|
name: user:
|
|
{
|
|
isNormalUser = user.isNormalUser or true;
|
|
description = user.description or name;
|
|
extraGroups = user.extraGroups or [ ];
|
|
}
|
|
// lib.optionalAttrs (user ? hashedPasswordFile && user.hashedPasswordFile != null) {
|
|
hashedPasswordFile = user.hashedPasswordFile;
|
|
};
|
|
|
|
hmWiredNames =
|
|
selectedUsers:
|
|
lib.attrNames (
|
|
lib.filterAttrs (
|
|
_: user:
|
|
let
|
|
hm = userHm user;
|
|
in
|
|
(hm.enable or false) && (hm.module or null) != null
|
|
) selectedUsers
|
|
);
|
|
|
|
rbwOutboundSnippet =
|
|
name: user:
|
|
let
|
|
outboundCfg = ((userSsh user).outbound or { }).rbw or { };
|
|
in
|
|
lib.mkIf (outboundCfg.enable or false) {
|
|
chiasson.ssh.outbound.rbw.enable = true;
|
|
chiasson.ssh.outbound.rbw.user = name;
|
|
chiasson.ssh.outbound.rbw.hosts =
|
|
if (outboundCfg.hosts or "all") == "all" then [ "all" ] else outboundCfg.hosts;
|
|
chiasson.ssh.outbound.rbw.extraIdentities = outboundCfg.extraIdentities or [ ];
|
|
};
|
|
|
|
mkHmUserModule =
|
|
{ name, user, hostExtraModules }:
|
|
let
|
|
hm = userHm user;
|
|
hmModule = resolveHomeManagerModule hm.module;
|
|
in
|
|
lib.mkMerge (
|
|
[
|
|
hmModule
|
|
(rbwOutboundSnippet name user)
|
|
]
|
|
++ (hm.extraModules or [ ])
|
|
++ hostExtraModules
|
|
);
|
|
|
|
inboundAuthorizedOrNull =
|
|
user:
|
|
let
|
|
inboundCfg = (userSsh user).inbound or { };
|
|
in
|
|
if !(inboundCfg.enable or false) then
|
|
null
|
|
else if (inboundCfg.authorizedHosts or "all") == "all" then
|
|
"all"
|
|
else
|
|
inboundCfg.authorizedHosts;
|
|
|
|
inboundHostsAttr =
|
|
selectedUsers:
|
|
lib.pipe selectedUsers [
|
|
(lib.mapAttrs (_: inboundAuthorizedOrNull))
|
|
(lib.filterAttrs (_: v: v != null))
|
|
];
|
|
};
|
|
inventory = self.lib.sshInventory;
|
|
|
|
olivierEnabled = lib.elem "olivier" cfg.enabled;
|
|
|
|
# Merge catalog + per-host overrides into the final user attrs.
|
|
selectedUsers = lib.listToAttrs (
|
|
map (name: {
|
|
inherit name;
|
|
value = lib.recursiveUpdate cfg.catalog.${name} (cfg.hostOverrides.${name} or { });
|
|
}) cfg.enabled
|
|
);
|
|
|
|
names = usersLib.hmWiredNames selectedUsers;
|
|
missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled;
|
|
stray = usersLib.strayHomeUserKeys cfg.extraModules cfg.enabled;
|
|
hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options;
|
|
inboundUsersAttr = usersLib.inboundHostsAttr selectedUsers;
|
|
|
|
hmUsersAttr = lib.listToAttrs (
|
|
map (name: {
|
|
inherit name;
|
|
value = usersLib.mkHmUserModule {
|
|
inherit name;
|
|
user = selectedUsers.${name};
|
|
hostExtraModules = cfg.extraModules.${name} or [ ];
|
|
};
|
|
}) names
|
|
);
|
|
|
|
# Fish shell wiring: HM's fish module declares its package, but
|
|
# /etc/passwd + /etc/shells need updating so login shells work.
|
|
# Only fires for HM users that actually enable fish.
|
|
hmFishUsers =
|
|
if !hmAvailable then
|
|
{ }
|
|
else
|
|
lib.filterAttrs (
|
|
name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names
|
|
) config.home-manager.users;
|
|
in
|
|
{
|
|
imports = [
|
|
self.nixosModules.sshInbound
|
|
{ _module.args = { inherit self usersLib; }; }
|
|
];
|
|
|
|
####################
|
|
# Options
|
|
####################
|
|
options.chiasson.users = {
|
|
catalog = lib.mkOption {
|
|
type = lib.types.attrs;
|
|
default = { };
|
|
description = "User records; defaults below. Override with `hostOverrides` or `mkForce`.";
|
|
};
|
|
enabled = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Catalog names to materialize as `users.users` on this machine.";
|
|
};
|
|
hostOverrides = lib.mkOption {
|
|
type = lib.types.attrs;
|
|
default = { };
|
|
description = "`recursiveUpdate`'d onto catalog users on this host.";
|
|
};
|
|
# Standard `attrsOf (listOf …)` already concatenates lists across
|
|
# module definitions; no custom merge function needed.
|
|
extraModules = lib.mkOption {
|
|
type = lib.types.attrsOf (lib.types.listOf lib.types.unspecified);
|
|
default = { };
|
|
description = ''
|
|
Per-user Home Manager extraModules keyed by catalog name. Lists
|
|
from multiple modules (desktopHomeBase + this host's home.nix)
|
|
are concatenated.
|
|
'';
|
|
};
|
|
homeManager.autoWire = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Auto-create Home Manager users from the catalog.";
|
|
};
|
|
};
|
|
|
|
####################
|
|
# Catalog defaults + integration
|
|
####################
|
|
# Nix won't accept both `config.X = …` and `config = …` in the same
|
|
# attrset (the path-attr partially defines `config`, then the explicit
|
|
# `config = …` is a redefinition error). All `config.` writes must
|
|
# therefore live INSIDE the single mkMerge block below.
|
|
config = lib.mkMerge [
|
|
####################
|
|
# Catalog defaults
|
|
####################
|
|
{
|
|
chiasson.users.catalog = {
|
|
olivier = {
|
|
isNormalUser = true;
|
|
description = "Olivier";
|
|
extraGroups = [
|
|
"networkmanager"
|
|
"wheel"
|
|
"docker"
|
|
"fuse"
|
|
"uinput"
|
|
"kvm"
|
|
# `video` lets brightnessctl/light udev rules own /sys/backlight without sudo.
|
|
# Harmless on headless hosts (no devices).
|
|
"video"
|
|
# DRI render + input for gamescope / Steam on Wayland, no sudo.
|
|
"render"
|
|
"input"
|
|
];
|
|
hashedPasswordFile = lib.mkIf olivierEnabled (
|
|
config.sops.secrets."users/olivier/hashedPassword".path
|
|
);
|
|
homeManager = {
|
|
enable = true;
|
|
module = { pkgs, ... }: {
|
|
home.username = "olivier";
|
|
home.homeDirectory = "/home/olivier";
|
|
home.stateVersion = "25.11";
|
|
programs.home-manager.enable = true;
|
|
|
|
# Declarative rbw config — replaces per-host
|
|
# `rbw config set email|base_url`. Neither value is a
|
|
# secret (URL + login email), so plaintext is fine, same
|
|
# as the public keys committed in ssh-inventory.nix.
|
|
programs.rbw = {
|
|
enable = true;
|
|
settings = {
|
|
email = "olivierchiasson@hotmail.fr";
|
|
base_url = "https://bitwarden.chiasson.cloud";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
ssh.inbound.enable = true;
|
|
ssh.inbound.authorizedHosts = "all";
|
|
ssh.outbound.rbw.enable = true;
|
|
ssh.outbound.rbw.hosts = [ "all" ];
|
|
# olivier's laptop: the HM SSH module writes the matching
|
|
# `.pub` filter for each of these and emits a `Match user`
|
|
# block, so `ssh server@r5500` / `ssh builder@nix-server`
|
|
# route the right private key from the rbw agent.
|
|
ssh.outbound.rbw.extraIdentities = [
|
|
"server"
|
|
"builder"
|
|
];
|
|
};
|
|
|
|
server = {
|
|
isNormalUser = true;
|
|
description = "Server user";
|
|
extraGroups = [ "wheel" ];
|
|
homeManager = {
|
|
enable = false;
|
|
module = null;
|
|
};
|
|
ssh.inbound.enable = true;
|
|
ssh.inbound.authorizedHosts = "all";
|
|
ssh.outbound.rbw.enable = false;
|
|
ssh.outbound.rbw.hosts = [ "all" ];
|
|
};
|
|
|
|
builder = {
|
|
isNormalUser = true;
|
|
description = "Navi fleet deploy (push + activate only)";
|
|
extraGroups = [ ];
|
|
createHome = false;
|
|
homeManager = {
|
|
enable = false;
|
|
module = null;
|
|
};
|
|
ssh.inbound.enable = true;
|
|
ssh.inbound.authorizedHosts = "all";
|
|
};
|
|
};
|
|
|
|
# Pull in olivier's hashed password from sops; `neededForUsers`
|
|
# makes nixos decrypt it at boot so the user record can read it.
|
|
sops.secrets."users/olivier/hashedPassword" = lib.mkIf olivierEnabled {
|
|
neededForUsers = true;
|
|
};
|
|
}
|
|
|
|
####################
|
|
# Integration
|
|
####################
|
|
{
|
|
assertions = [
|
|
{
|
|
assertion = missing == [ ];
|
|
message = "chiasson.users.enabled references unknown catalog names: ${lib.concatStringsSep ", " missing}";
|
|
}
|
|
{
|
|
assertion = stray == [ ];
|
|
message = "chiasson.users.extraModules has keys not in chiasson.users.enabled: ${lib.concatStringsSep ", " stray}";
|
|
}
|
|
];
|
|
|
|
# NixOS user accounts (no HM, raw).
|
|
users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selectedUsers;
|
|
}
|
|
(lib.optionalAttrs hmAvailable {
|
|
"home-manager".useGlobalPkgs = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) true;
|
|
"home-manager".sharedModules = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) [
|
|
self.homeManagerModules.sshOutboundRbw
|
|
];
|
|
"home-manager".users = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) hmUsersAttr;
|
|
})
|
|
# Wire user-specific ACLs into the inbound module — fail the build
|
|
# if a user has no matching pubkey.
|
|
(lib.mkIf (inboundUsersAttr != { }) {
|
|
chiasson.ssh.inbound.enable = true;
|
|
chiasson.ssh.inbound.userAuthorizedHosts = inboundUsersAttr;
|
|
})
|
|
# Fish-shell wiring: HM knows the package; we sync /etc/passwd +
|
|
# /etc/shells with `mkForce`. `mkNixosUser` doesn't emit `shell`, so
|
|
# the force is uncontested.
|
|
(lib.mkIf (hmFishUsers != { }) {
|
|
environment.shells = lib.mkAfter (
|
|
lib.mapAttrsToList (_: hmUser: lib.getExe hmUser.programs.fish.package) hmFishUsers
|
|
);
|
|
users.users = lib.mapAttrs (name: hmUser: {
|
|
shell = lib.mkForce (lib.getExe hmUser.programs.fish.package);
|
|
}) hmFishUsers;
|
|
})
|
|
];
|
|
};
|
|
}
|