Refactor ssh&users
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# 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 = self.lib.usersMerge lib;
|
||||
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 = { ... }: {
|
||||
home.username = "olivier";
|
||||
home.homeDirectory = "/home/olivier";
|
||||
home.stateVersion = "25.11";
|
||||
programs.home-manager.enable = true;
|
||||
};
|
||||
};
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
# 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;
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
# Shared user definitions for all hosts that import `nixosModules.users`.
|
||||
# Module (not bare attrset) so catalog entries can use `config.*` for sops paths etc.
|
||||
{ ... }: {
|
||||
flake.nixosModules.usersCatalogDefaults =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
olivierEnabled = lib.elem "olivier" config.chiasson.users.enabled;
|
||||
in
|
||||
{
|
||||
config.sops.secrets."users/olivier/hashedPassword" = lib.mkIf olivierEnabled {
|
||||
neededForUsers = true;
|
||||
};
|
||||
|
||||
config.chiasson.users.catalog = {
|
||||
olivier = {
|
||||
isNormalUser = true;
|
||||
description = "Olivier";
|
||||
extraGroups = [
|
||||
"networkmanager"
|
||||
"wheel"
|
||||
"docker"
|
||||
"fuse"
|
||||
"uinput"
|
||||
"kvm"
|
||||
# `video` is required for the brightnessctl/light udev rules to grant write access
|
||||
# to /sys/class/backlight/*/brightness without sudo. Harmless on hosts without a
|
||||
# backlight (servers, desktop towers): the group simply has no devices to own.
|
||||
"video"
|
||||
# DRI render nodes and input devices for gamescope / Steam on Wayland (no sudo).
|
||||
"render"
|
||||
"input"
|
||||
];
|
||||
|
||||
# Declared in this module when olivier is in `chiasson.users.enabled`.
|
||||
# With `neededForUsers`, `.path` is under /run/secrets-for-users/… (sops-nix README).
|
||||
hashedPasswordFile = lib.mkIf olivierEnabled (
|
||||
config.sops.secrets."users/olivier/hashedPassword".path
|
||||
);
|
||||
|
||||
homeManager = {
|
||||
enable = true;
|
||||
module =
|
||||
{ ... }:
|
||||
{
|
||||
home.username = "olivier";
|
||||
home.homeDirectory = "/home/olivier";
|
||||
home.stateVersion = "25.11";
|
||||
programs.home-manager.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
ssh = {
|
||||
inbound = {
|
||||
enable = true;
|
||||
authorizedHosts = "all";
|
||||
};
|
||||
outbound = {
|
||||
rbw = {
|
||||
enable = true;
|
||||
hosts = "all";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
server = {
|
||||
isNormalUser = true;
|
||||
description = "Server user";
|
||||
extraGroups = [ "wheel" ];
|
||||
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
|
||||
ssh = {
|
||||
inbound = {
|
||||
enable = true;
|
||||
authorizedHosts = "all";
|
||||
};
|
||||
outbound = {
|
||||
rbw = {
|
||||
enable = false;
|
||||
hosts = "all";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
builder = {
|
||||
isNormalUser = true;
|
||||
description = "Navi fleet deploy (push + activate only)";
|
||||
extraGroups = [ ];
|
||||
createHome = false;
|
||||
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
|
||||
ssh.inbound.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{ ... }: {
|
||||
flake.nixosModules.usersCatalogOptions =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.chiasson.users = {
|
||||
catalog = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
User records merged from `usersCatalogDefaults`; 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.
|
||||
'';
|
||||
};
|
||||
extraModules = lib.mkOption {
|
||||
type =
|
||||
(lib.types.attrsOf (lib.types.listOf lib.types.unspecified))
|
||||
// {
|
||||
merge =
|
||||
loc: defs:
|
||||
let
|
||||
values = map (d: d.value) defs;
|
||||
names = lib.unique (lib.concatLists (map builtins.attrNames values));
|
||||
in
|
||||
lib.genAttrs names (
|
||||
name: lib.concatLists (map (v: v.${name} or [ ]) values)
|
||||
);
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Per-user Home Manager `extraModules` keyed by catalog user name.
|
||||
Keys must match `chiasson.users.enabled`. Lists from multiple modules
|
||||
are concatenated (e.g. `desktop-home-base.nix` + host `home.nix`).
|
||||
'';
|
||||
};
|
||||
homeManager = {
|
||||
autoWire = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Create HM users from the catalog when true.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
# Catalog → NixOS `users.users` + Home Manager + SSH inbound.
|
||||
{ self, ... }: {
|
||||
flake.nixosModules.users =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
usersLib = self.lib.usersMerge lib;
|
||||
selectUsers =
|
||||
c:
|
||||
let
|
||||
uc = c.chiasson.users;
|
||||
in
|
||||
usersLib.selectedUsersAttr {
|
||||
catalog = uc.catalog;
|
||||
enabled = uc.enabled;
|
||||
hostOverrides = uc.hostOverrides;
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
self.nixosModules.sshInbound
|
||||
self.nixosModules.usersCatalogOptions
|
||||
self.nixosModules.usersCatalogDefaults
|
||||
{ _module.args = { inherit self usersLib selectUsers; }; }
|
||||
self.nixosModules.usersHomeIntegration
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
{ ... }: {
|
||||
flake.nixosModules.usersHomeIntegration =
|
||||
{ config, options, lib, self, usersLib, selectUsers, ... }:
|
||||
let
|
||||
cfg = config.chiasson.users;
|
||||
selected = selectUsers config;
|
||||
missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled;
|
||||
stray = usersLib.strayHomeUserKeys cfg.extraModules cfg.enabled;
|
||||
names = usersLib.hmWiredNames selected;
|
||||
hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options;
|
||||
hmUsersAttr = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = usersLib.mkHmUserModule {
|
||||
inherit name;
|
||||
user = selected.${name};
|
||||
hostExtraModules = cfg.extraModules.${name} or [ ];
|
||||
};
|
||||
}) names
|
||||
);
|
||||
inboundUsersAttr = usersLib.inboundHostsAttr selected;
|
||||
|
||||
# HM configures fish in ~/.config/fish but no longer sets /etc/passwd or /etc/shells.
|
||||
hmFishUsers =
|
||||
if !hmAvailable then { }
|
||||
else
|
||||
lib.filterAttrs (
|
||||
name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names
|
||||
) config.home-manager.users;
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missing == [ ];
|
||||
message = "chiasson.users.enabled references unknown catalog users: ${builtins.concatStringsSep ", " missing}";
|
||||
}
|
||||
{
|
||||
assertion = stray == [ ];
|
||||
message = "chiasson.users.extraModules has keys not in chiasson.users.enabled: ${builtins.concatStringsSep ", " stray}";
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selected;
|
||||
}
|
||||
(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;
|
||||
})
|
||||
(lib.mkIf (inboundUsersAttr != { }) {
|
||||
chiasson.ssh.inbound.enable = true;
|
||||
chiasson.ssh.inbound.userAuthorizedHosts = inboundUsersAttr;
|
||||
})
|
||||
(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;
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user