Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c90d368432 | |||
| a787840ce2 |
+5
-5
@@ -47,7 +47,7 @@ Leave upstream `services.*`, `networking.*`, `home.*`, etc. alone.
|
||||
|
||||
Aggregates: `nixosModules.system` and `nixosModules.desktop` import their leaves. Host configs import those stacks and set options — they shouldn't reimplement whole subsystems.
|
||||
|
||||
**Home** — files under `modules/wisdom/`. Baseline is `homeManagerModules.wisdom` (`chiasson.home.enable`, wired via `chiasson.desktop.homeManager.bundleWisdom`). Other `wisdom*` slices auto-wire once per user via `lib.wisdomCatalogExtraModules self` (`modules/lib/wisdom-catalog.nix`); hosts only set matching `chiasson.home.*.enable` toggles — no re-import in `home.nix`.
|
||||
**Home** — files under `modules/wisdom/`. Baseline is `homeManagerModules.wisdom` (`chiasson.home.enable`, wired via `chiasson.desktop.homeManager.bundleWisdom`). Other `wisdom*` slices auto-wire once per user via `lib.wisdomCatalogExtraModules self` (`modules/desktop/options.nix`); hosts only set matching `chiasson.home.*.enable` toggles — no re-import in `home.nix`.
|
||||
|
||||
User apps / dotfiles → wisdom. Daemons, firewall, kernel → NixOS. Sometimes both (LocalSend: HM installs, `systemLocalsend` opens the firewall).
|
||||
|
||||
@@ -76,9 +76,9 @@ SSH authentication has two independent sides:
|
||||
| **Client** (connecting from) | identity private keys | "Here's proof I own key K" |
|
||||
| **Server** (connecting to) | `authorized_keys` | "Do I trust key K?" |
|
||||
|
||||
A key is just a cryptographic identity — it's not bound to a host or a machine account name. The model is **one key per user account**: each catalog account (`olivier`, `server`, `builder`) has its own keypair in `modules/lib/ssh-inventory.nix`, regardless of which machine that account runs on. Concretely: when `olivier@14900k` sshes to `server@r5500`, OpenSSH routes via a `Match user server` block (generated from `chiasson.ssh.outbound.rbw.extraIdentities`) → `IdentityFile ~/.ssh/id_ed25519_server.pub` (written from `users.server.publicKey`) to ask the rbw agent for the `server` private key; r5500's `server` `authorized_keys` holds the matching server pubkey. olivier's catalog default sets `extraIdentities = [ "server" "builder" ]` so `ssh <account>@<host>` Just Works for every catalog role. We also support `from="ip,cidr"` constraints per account to scope which source IPs may authenticate as it.
|
||||
A key is just a cryptographic identity — it's not bound to a host or a machine account name. The model is **one key per user account**: each catalog account (`olivier`, `server`, `builder`) has its own keypair in `modules/ssh/inventory.nix`, regardless of which machine that account runs on. Concretely: when `olivier@14900k` sshes to `server@r5500`, OpenSSH routes via a `Match user server` block (generated from `chiasson.ssh.outbound.rbw.extraIdentities`) → `IdentityFile ~/.ssh/id_ed25519_server.pub` (written from `users.server.publicKey`) to ask the rbw agent for the `server` private key; r5500's `server` `authorized_keys` holds the matching server pubkey. olivier's catalog default sets `extraIdentities = [ "server" "builder" ]` so `ssh <account>@<host>` Just Works for every catalog role. We also support `from="ip,cidr"` constraints per account to scope which source IPs may authenticate as it.
|
||||
|
||||
This repo enforces "one key per user": every user has one ed25519 keypair, the private key in Bitwarden, the public key inline in `modules/lib/ssh-inventory.nix`. On SSH connect:
|
||||
This repo enforces "one key per user": every user has one ed25519 keypair, the private key in Bitwarden, the public key inline in `modules/ssh/inventory.nix`. On SSH connect:
|
||||
|
||||
1. `~/.ssh/config` (managed by home-manager) sets `IdentityAgent SSH_AUTH_SOCK` and `IdentityFile ~/.ssh/id_ed25519_<user>.pub` for every host block.
|
||||
2. OpenSSH reads the `.pub` file as a **filter** against the agent (OpenSSH 7.3+ trick — `.pub` as IdentityFile tells the agent "give me the private key matching this pub"), so even with multiple keys loaded, only the matching one is tried. This is why the old "too many auth failures" symptom is gone by construction.
|
||||
@@ -105,7 +105,7 @@ ssh root@nix-server nixos-rebuild switch --flake .#nix-server
|
||||
|
||||
### Adding a host
|
||||
|
||||
Edit `modules/lib/ssh-inventory.nix` and add:
|
||||
Edit `modules/ssh/inventory.nix` and add:
|
||||
|
||||
```nix
|
||||
hosts."newhost" = {
|
||||
@@ -119,7 +119,7 @@ Then add `self.nixosModules.<host>Configuration` to `modules/deploy/navi.nix`.
|
||||
### Adding a catalog user
|
||||
|
||||
1. Generate a keypair: `rbw gen ssh-key <name>`.
|
||||
2. Add a `users.<name> = { publicKey = "ssh-ed25519 …"; };` line under `users` in `modules/lib/ssh-inventory.nix`.
|
||||
2. Add a `users.<name> = { publicKey = "ssh-ed25519 …"; };` line under `users` in `modules/ssh/inventory.nix`.
|
||||
3. Add a `<name> = { isNormalUser = true; … ssh.inbound.enable = true; ssh.inbound.authorizedHosts = "all"; }` entry under `chiasson.users.catalog` in `modules/system/users.nix`.
|
||||
4. Enable on every host that needs that user, e.g.:
|
||||
|
||||
|
||||
+32
-1
@@ -98,9 +98,40 @@ let
|
||||
metaNixpkgs = import inputs.nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
# Build a raw Navi hive attrset from host specs + deployment targets.
|
||||
# `inputs.navi.lib.makeHive` turns the result into `flake.naviHive`.
|
||||
mkNaviHiveConfig =
|
||||
{
|
||||
metaNixpkgs,
|
||||
hostSpecs,
|
||||
deployments,
|
||||
}:
|
||||
let
|
||||
deployNodes = lib.filterAttrs (name: _: deployments ? ${name}) hostSpecs;
|
||||
in
|
||||
{
|
||||
meta = {
|
||||
nixpkgs = metaNixpkgs;
|
||||
nodeNixpkgs = lib.mapAttrs (
|
||||
name: spec:
|
||||
import inputs.nixpkgs {
|
||||
system = spec.system;
|
||||
}
|
||||
) deployNodes;
|
||||
nodeSpecialArgs = lib.mapAttrs (_: spec: spec.specialArgs) deployNodes;
|
||||
allowApplyAll = false;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs (
|
||||
name: spec:
|
||||
{
|
||||
imports = spec.modules or [ spec.configuration ];
|
||||
deployment = deployments.${name};
|
||||
}
|
||||
) deployNodes;
|
||||
in
|
||||
{
|
||||
flake.navi = self.lib.mkNaviHiveConfig {
|
||||
flake.navi = mkNaviHiveConfig {
|
||||
inherit metaNixpkgs hostSpecs deployments;
|
||||
};
|
||||
|
||||
|
||||
+46
-24
@@ -1,4 +1,19 @@
|
||||
{ ... }: {
|
||||
{ lib, ... }: {
|
||||
# Resolve `wisdom*` HM slices for NixOS `extraModules` (`self` is not an HM specialArg).
|
||||
# Slices are gated by `chiasson.home.*.enable`; hosts only flip toggles in `home.nix`.
|
||||
# Lives here (not `modules/lib/`) because `hosts/desktop-home-base.nix` consumes it and
|
||||
# fragment eval order requires the definition to sort before `hosts/`.
|
||||
flake.lib.wisdomCatalogExtraModules =
|
||||
self:
|
||||
let
|
||||
names = lib.sort builtins.lessThan (
|
||||
lib.filter (n: lib.hasPrefix "wisdom" n && n != "wisdom" && n != "wisdomShellBash") (
|
||||
builtins.attrNames self.homeManagerModules
|
||||
)
|
||||
);
|
||||
in
|
||||
map (name: self.homeManagerModules.${name}) names;
|
||||
|
||||
flake.nixosModules.desktopOptions =
|
||||
{ config, options, lib, pkgs, self, inputs, ... }:
|
||||
let
|
||||
@@ -34,11 +49,13 @@
|
||||
{
|
||||
options.chiasson.desktop = {
|
||||
defaultSession = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.enum [
|
||||
"hyprland"
|
||||
"niri"
|
||||
"plasma"
|
||||
]);
|
||||
type = lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"hyprland"
|
||||
"niri"
|
||||
"plasma"
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
example = "niri";
|
||||
description = ''
|
||||
@@ -56,10 +73,12 @@
|
||||
|
||||
displayManager = {
|
||||
variant = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.enum [
|
||||
"sddm"
|
||||
"dankgreeter"
|
||||
]);
|
||||
type = lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"sddm"
|
||||
"dankgreeter"
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
description = ''
|
||||
SDDM vs DankGreeter (greetd + DMS — [docs](https://danklinux.com/docs/dankgreeter/nixos-flake)).
|
||||
@@ -80,11 +99,13 @@
|
||||
};
|
||||
|
||||
displayManager.sddm = {
|
||||
wayland.enable = lib.mkEnableOption ''
|
||||
SDDM greeter on Wayland (nicer on HiDPI; turn off if the greeter glitches on your GPU).
|
||||
'' // {
|
||||
default = true;
|
||||
};
|
||||
wayland.enable =
|
||||
lib.mkEnableOption ''
|
||||
SDDM greeter on Wayland (nicer on HiDPI; turn off if the greeter glitches on your GPU).
|
||||
''
|
||||
// {
|
||||
default = true;
|
||||
};
|
||||
|
||||
enableHidpi = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
@@ -159,12 +180,14 @@
|
||||
};
|
||||
|
||||
keyring = {
|
||||
enable = lib.mkEnableOption ''
|
||||
gnome-keyring + pam (login + sddm or greetd) + HM user service + `gcr` + `services.xserver.updateDbusEnvironment`.
|
||||
niri/hyprland: `dbus-update-activation-environment` at compositor start so libsecret/Electron see `WAYLAND_DISPLAY`.
|
||||
'' // {
|
||||
default = true;
|
||||
};
|
||||
enable =
|
||||
lib.mkEnableOption ''
|
||||
gnome-keyring + pam (login + sddm or greetd) + HM user service + `gcr` + `services.xserver.updateDbusEnvironment`.
|
||||
niri/hyprland: `dbus-update-activation-environment` at compositor start so libsecret/Electron see `WAYLAND_DISPLAY`.
|
||||
''
|
||||
// {
|
||||
default = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -184,8 +207,7 @@
|
||||
message = "chiasson.desktop.defaultSession = \"plasma\" requires chiasson.desktop.plasma.enable = true.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.displayManager.variant != "dankgreeter" || cfg.hyprland.enable || cfg.niri.enable;
|
||||
assertion = cfg.displayManager.variant != "dankgreeter" || cfg.hyprland.enable || cfg.niri.enable;
|
||||
message = "chiasson.desktop.displayManager.variant = \"dankgreeter\" requires chiasson.desktop.hyprland or chiasson.desktop.niri.";
|
||||
}
|
||||
{
|
||||
@@ -210,7 +232,7 @@
|
||||
(lib.mkIf (dmsEnabled && hmAvailable) {
|
||||
"home-manager".sharedModules = [ self.homeManagerModules.desktopShellDms ];
|
||||
})
|
||||
(lib.mkIf (hmAvailable && (dmsEnabled || cfg.niri.enable)) {
|
||||
(lib.mkIf hmAvailable {
|
||||
"home-manager".extraSpecialArgs = { inherit inputs; };
|
||||
})
|
||||
];
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
spotify.openDiscoveryFirewall = true;
|
||||
pokeclicker.enable = true;
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@@ -4,18 +4,5 @@
|
||||
{ self, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.desktopHomeBase ];
|
||||
|
||||
# Host-only HM overrides (tablet apps, toggles off from desktop-home-base, …).
|
||||
chiasson.users.extraModules.olivier = [
|
||||
# {
|
||||
# chiasson.home = {
|
||||
# # editors.kate.enable = true;
|
||||
# };
|
||||
# }
|
||||
# (
|
||||
# { pkgs, ... }:
|
||||
# { home.packages = with pkgs; [ ]; }
|
||||
# )
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
chiasson.users.extraModules.olivier = [
|
||||
{
|
||||
chiasson.home = {
|
||||
browsers = {
|
||||
edge.enable = true;
|
||||
};
|
||||
browsers.edge.enable = true;
|
||||
|
||||
apps = {
|
||||
discord.enable = true;
|
||||
@@ -19,6 +17,5 @@
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
chiasson.users.extraModules.olivier = [
|
||||
{
|
||||
chiasson.home = {
|
||||
hardware.uconsoleGamepad.enable = true;
|
||||
};
|
||||
chiasson.home.hardware.uconsoleGamepad.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Build a raw Navi hive attrset from host specs + deployment targets.
|
||||
# Call `inputs.navi.lib.makeHive` on the result to produce `flake.naviHive`.
|
||||
{ lib, inputs, ... }: {
|
||||
flake.lib.mkNaviHiveConfig =
|
||||
{
|
||||
metaNixpkgs,
|
||||
hostSpecs,
|
||||
deployments,
|
||||
}:
|
||||
let
|
||||
deployNodes = lib.filterAttrs (name: _: deployments ? ${name}) hostSpecs;
|
||||
in
|
||||
{
|
||||
meta = {
|
||||
nixpkgs = metaNixpkgs;
|
||||
nodeNixpkgs = lib.mapAttrs (
|
||||
name: spec:
|
||||
import inputs.nixpkgs {
|
||||
system = spec.system;
|
||||
}
|
||||
) deployNodes;
|
||||
nodeSpecialArgs = lib.mapAttrs (_: spec: spec.specialArgs) deployNodes;
|
||||
allowApplyAll = false;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs (
|
||||
name: spec:
|
||||
{
|
||||
imports = spec.modules or [ spec.configuration ];
|
||||
deployment = deployments.${name};
|
||||
}
|
||||
) deployNodes;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{ ... }: {
|
||||
flake.lib.rbwSshSocket =
|
||||
let
|
||||
# Bash/Fish init snippets — used by wisdom/shells/{bash,fish}.nix.
|
||||
bashSnippet = ''
|
||||
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
|
||||
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
|
||||
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
|
||||
else
|
||||
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
fishSnippet =
|
||||
pkgs: ''
|
||||
if test -z "$SSH_AUTH_SOCK"
|
||||
if test -n "$XDG_RUNTIME_DIR"
|
||||
set -gx SSH_AUTH_SOCK "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
|
||||
else
|
||||
set -l _hm_uid (${pkgs.coreutils}/bin/id -u)
|
||||
set -gx SSH_AUTH_SOCK "/run/user/$_hm_uid/rbw/ssh-agent-socket"
|
||||
end
|
||||
end
|
||||
'';
|
||||
in {
|
||||
inherit bashSnippet fishSnippet;
|
||||
# Used by home-manager modules to expose SSH_AUTH_SOCK to GUI apps
|
||||
# and HM-managed shells declaratively (no activation script needed).
|
||||
sessionVariable = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
|
||||
};
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# Pure helpers: catalog → NixOS/HM/SSH shapes (`self.lib.usersMerge lib`).
|
||||
{ ... }: {
|
||||
flake.lib.usersMerge =
|
||||
lib:
|
||||
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))
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# Resolve `wisdom*` HM slices for NixOS `extraModules` (`self` is not an HM specialArg).
|
||||
# Slices are gated by `chiasson.home.*.enable`; hosts only flip toggles in `home.nix`.
|
||||
{ lib, ... }: {
|
||||
flake.lib.wisdomCatalogExtraModules =
|
||||
self:
|
||||
let
|
||||
names = lib.sort builtins.lessThan (
|
||||
lib.filter (
|
||||
n:
|
||||
lib.hasPrefix "wisdom" n
|
||||
&& n != "wisdom"
|
||||
&& n != "wisdomShellBash"
|
||||
) (builtins.attrNames self.homeManagerModules)
|
||||
);
|
||||
in
|
||||
map (name: self.homeManagerModules.${name}) names;
|
||||
}
|
||||
@@ -7,10 +7,7 @@
|
||||
# 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;
|
||||
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.
|
||||
@@ -18,20 +15,21 @@
|
||||
if cfg.hosts == [ "all" ] then
|
||||
inventory.activeHosts
|
||||
else
|
||||
builtins.listToAttrs (map (n: { name = n; value = inventory.hosts.${n}; }) selectedHostNames);
|
||||
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 "") != "";
|
||||
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;
|
||||
missingExtraKeys = lib.filter (n: (inventory.users.${n}.publicKey or "") == "") cfg.extraIdentities;
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.outbound.rbw = {
|
||||
@@ -64,11 +62,14 @@
|
||||
extraIdentities = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "server" "builder" ];
|
||||
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
|
||||
`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.
|
||||
@@ -76,73 +77,78 @@
|
||||
};
|
||||
};
|
||||
|
||||
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 [
|
||||
config = lib.mkIf cfg.enable (
|
||||
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;
|
||||
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.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" ];
|
||||
(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;
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'";
|
||||
RemainAfterExit = true;
|
||||
})
|
||||
{
|
||||
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" ];
|
||||
};
|
||||
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
|
||||
'';
|
||||
}
|
||||
])
|
||||
]);
|
||||
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
|
||||
'';
|
||||
}
|
||||
])
|
||||
]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{
|
||||
assertions = [{
|
||||
assertion = cfg.enable -> missingKeys == [ ];
|
||||
message = "chiasson.ssh.inbound: users enabled but missing public key in `modules/lib/ssh-inventory.nix`: ${lib.concatStringsSep ", " missingKeys}";
|
||||
message = "chiasson.ssh.inbound: users enabled but missing public key in `modules/ssh/inventory.nix`: ${lib.concatStringsSep ", " missingKeys}";
|
||||
}];
|
||||
}
|
||||
(lib.mkIf cfg.enable {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# rbw (Bitwarden) SSH-agent socket wiring, shared across shells and the
|
||||
# outbound-rbw Home Manager module. Exposed as `flake.lib.rbwSshSocket`.
|
||||
{ ... }: {
|
||||
flake.lib.rbwSshSocket =
|
||||
let
|
||||
socketPath = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
|
||||
|
||||
# Bash init snippet — used by wisdom/shells/bash.nix (profile + interactive).
|
||||
bashSnippet = ''
|
||||
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
|
||||
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
|
||||
export SSH_AUTH_SOCK="${socketPath}"
|
||||
else
|
||||
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# Fish init snippet — used by wisdom/shells/fish.nix.
|
||||
fishSnippet = pkgs: ''
|
||||
if test -z "$SSH_AUTH_SOCK"
|
||||
if test -n "$XDG_RUNTIME_DIR"
|
||||
set -gx SSH_AUTH_SOCK "${socketPath}"
|
||||
else
|
||||
set -l _hm_uid (${pkgs.coreutils}/bin/id -u)
|
||||
set -gx SSH_AUTH_SOCK "/run/user/$_hm_uid/rbw/ssh-agent-socket"
|
||||
end
|
||||
end
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit bashSnippet fishSnippet;
|
||||
# Used by home-manager modules to expose SSH_AUTH_SOCK to GUI apps
|
||||
# and HM-managed shells declaratively (no activation script needed).
|
||||
sessionVariable = socketPath;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+138
-27
@@ -8,18 +8,115 @@
|
||||
{ config, options, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.users;
|
||||
usersLib = self.lib.usersMerge lib;
|
||||
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: {
|
||||
selectedUsers = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate cfg.catalog.${name} (cfg.hostOverrides.${name} or { });
|
||||
})
|
||||
cfg.enabled);
|
||||
}) cfg.enabled
|
||||
);
|
||||
|
||||
names = usersLib.hmWiredNames selectedUsers;
|
||||
missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled;
|
||||
@@ -27,26 +124,27 @@
|
||||
hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options;
|
||||
inboundUsersAttr = usersLib.inboundHostsAttr selectedUsers;
|
||||
|
||||
hmUsersAttr = lib.listToAttrs (map
|
||||
(name: {
|
||||
hmUsersAttr = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = usersLib.mkHmUserModule {
|
||||
inherit name;
|
||||
user = selectedUsers.${name};
|
||||
hostExtraModules = cfg.extraModules.${name} or [ ];
|
||||
};
|
||||
})
|
||||
names);
|
||||
}) 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 { }
|
||||
if !hmAvailable then
|
||||
{ }
|
||||
else
|
||||
lib.filterAttrs
|
||||
(name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names)
|
||||
config.home-manager.users;
|
||||
lib.filterAttrs (
|
||||
name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names
|
||||
) config.home-manager.users;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -108,12 +206,18 @@
|
||||
isNormalUser = true;
|
||||
description = "Olivier";
|
||||
extraGroups = [
|
||||
"networkmanager" "wheel" "docker" "fuse" "uinput" "kvm"
|
||||
"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"
|
||||
"render"
|
||||
"input"
|
||||
];
|
||||
hashedPasswordFile = lib.mkIf olivierEnabled (
|
||||
config.sops.secrets."users/olivier/hashedPassword".path
|
||||
@@ -147,14 +251,20 @@
|
||||
# `.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" ];
|
||||
ssh.outbound.rbw.extraIdentities = [
|
||||
"server"
|
||||
"builder"
|
||||
];
|
||||
};
|
||||
|
||||
server = {
|
||||
isNormalUser = true;
|
||||
description = "Server user";
|
||||
extraGroups = [ "wheel" ];
|
||||
homeManager = { enable = false; module = null; };
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
ssh.outbound.rbw.enable = false;
|
||||
@@ -166,7 +276,10 @@
|
||||
description = "Navi fleet deploy (push + activate only)";
|
||||
extraGroups = [ ];
|
||||
createHome = false;
|
||||
homeManager = { enable = false; module = null; };
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
};
|
||||
@@ -195,9 +308,7 @@
|
||||
];
|
||||
|
||||
# NixOS user accounts (no HM, raw).
|
||||
users.users = lib.mapAttrs
|
||||
(name: user: usersLib.mkNixosUser name user)
|
||||
selectedUsers;
|
||||
users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selectedUsers;
|
||||
}
|
||||
(lib.optionalAttrs hmAvailable {
|
||||
"home-manager".useGlobalPkgs = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) true;
|
||||
@@ -216,12 +327,12 @@
|
||||
# /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;
|
||||
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