Files
chiasson-nix/docs/conventions.md
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

10 KiB

Conventions

How the flake is wired

flake.nix runs import-tree on ./modules with a few extra filters: skip /package/, /packages/, and dms/home-manager/ (callPackage noise + DMS HM colocation). Don't add a global /home-manager/ filter — modules/ssh/home-manager/ needs to load.

Every scanned .nix should be a flake fragment:

{ self, inputs, ... }: {
  flake.nixosModules.systemFoo = { config, lib, pkgs, ... }: { /* … */ };
}

Don't define the same flake.nixosModules.foo in two files — merge order isn't something to bet on.

Outputs I actually use: nixosConfigurations.*, nixosModules.*, homeManagerModules.*, lib.*, packages.*, navi / naviHive. modules/parts.nix declares supported systems and the flake.lib / flake.homeManagerModules option slots.

Option namespaces

Project options live under chiasson.* only:

Prefix For
chiasson.system.* Machine policy — docker, flatpak, audio, gaming, deploy builder, …
chiasson.desktop.* GUI — compositors, DMS, wallpapers, display manager
chiasson.home.* Home Manager toggles (module exports are still wisdom*)
chiasson.users.* Catalog, enabled, hostOverrides, extraModules
chiasson.ssh.* SSH inventory

Leave upstream services.*, networking.*, home.*, etc. alone.

Module shape

NixOS leaf — camelCase export, usually prefixed (systemDocker, desktopNiri):

{ ... }: {
  flake.nixosModules.systemSomething = { config, lib, pkgs, ... }:
  let cfg = config.chiasson.system.something;
  in {
    options.chiasson.system.something.enable = lib.mkEnableOption "…";
    config = lib.mkIf cfg.enable { /* … */ };
  };
}

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/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).

Odd exports

  • "client-services" — shared setup for laptops/tablets/desktops (not the VMs). Turns on Bluetooth, lets Navi log in as builder to update the machine, enables printing and mounting USB drives, and sets SSH to password. Used on t2mbp, 14900k, ideapad, uConsole. Skipped on nix-server and r5500.
  • systemUconsoleKernelBuilder — cross-build the uConsole kernel on x86_64, not for running on the Pi.
  • systemFlatpak — allowlist sync; skips heavy flatpak update when lists unchanged (runHeavyMaintenanceEverySwitch).

Desktop notes

  • chiasson.desktop.displayManager.variant: sddm vs dankgreeter (DMS + Hyprland/Niri tends toward DankGreeter).
  • desktopWallpapers copies inputs.wallpapers into the store; override chiasson.desktop.wallpapers.source if needed.

Users & SSH

User definitions live in the catalog defaults block inside modules/system/users.nix (was previously split across four files in modules/system/users/; folded into one for cohesion). olivier, server, builder, etc. Pick who exists on a host with chiasson.users.enabled.

SSH model — read this once

SSH authentication has two independent sides:

Side Holds Decides
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/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/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.
  3. rbw holds the private key; HM-managed shells + the desktop session expose SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/rbw/… declaratively (no activation scripts).

Bitwarden onboarding

For each new user (run on the user's primary machine):

# 1. Generate a keypair with rbw (creates a Bitwarden item + prints the pubkey).
rbw gen ssh-key olivier

# 2. Paste the printed `ssh-ed25519 AAAA… olivier` line into
#    modules/lib/ssh-inventory.nix under users.olivier.publicKey.

# 3. Push the user to a host:
ssh root@nix-server nixos-rebuild switch --flake .#nix-server

# 4. Have the user ssh into the fleet from any client — their rbw key
#    is the only thing offered, and every server already has it in
#    authorized_keys.

Adding a host

Edit modules/ssh/inventory.nix and add:

hosts."newhost" = {
  hostName = "192.168.2.X";        # IP — required for navi to deploy here
  aliases = [ "newhost" ];         # what you type at the ssh prompt
};

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/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.:
chiasson.users.enabled = [ "olivier" "<name>" ];

Passwords

Passwords aren't in the repo. They're in secrets/secrets.yaml (encrypted with sops). Each host that has olivier must declare that secret and set neededForUsers = true; sops-nix decrypts it at boot, and the catalog points hashedPasswordFile at that file. That's why the catalog is a proper module instead of a static list — it needs config.sops.secrets.… at eval time.

Host overrides

# configuration.nix — machine policy
chiasson.users.enabled = [ "olivier" ];
chiasson.users.hostOverrides.<name> = { /* optional */ };

Home-manager per-host slices (desktop/laptop hosts)

Desktop hosts also have home.nix exporting flake.nixosModules.<host>Home, wired from default.nix alongside *Configuration:

# default.nix
modules = [
  self.nixosModules."14900kConfiguration"
  self.nixosModules."14900kHome"
];

# home.nix — flake fragment, per-host chiasson.home.* toggles
{ self, inputs, ... }: {
  flake.nixosModules."14900kHome" = { self, pkgs, ... }: {
    imports = [ self.nixosModules.desktopHomeBase ];
    chiasson.users.extraModules.olivier = [
      {
        chiasson.home.browsers.edge.enable = true;
        # …
      }
    ];
  };
}

flake.nixosModules.desktopHomeBase expands lib.wisdomCatalogExtraModules plus shared desktop toggles. Host *Home modules append per-host chiasson.home overrides (and rare inline home.packages blocks). chiasson.users.extraModules concatenates lists from multiple modules (base + host), so both can set the same user key.

The integration block at the bottom of modules/system/users.nix turns the catalog + overrides into users.users + HM + inbound SSH. Don't hand-roll catalog users unless you're changing this module.

Adding things

New NixOS leaf: export flake.nixosModules.whatever, wire from system/default.nix or desktop/default.nix if it's global, or only from a host configuration.nix if it's not. nix flake check. Git-add new paths if eval uses the git tree.

New HM slice: add modules/wisdom/.../foo.nix exporting flake.homeManagerModules.wisdomFoo (import-tree picks it up; wisdomCatalogExtraModules includes every wisdom* export except wisdom / wisdomShellBash). Gate packages on chiasson.home.*.enable, set mkDefault true in desktop-home-base.nix if shared, or enable = true in a host home.nix. Upstream HM deps stay imported unconditionally — use mkIf on cfg.enable for config (never config-dependent imports; that recurses).

Derivations: let inside a fragment, or flake.packages / flake.lib — not a bare mkDerivation file import-tree will try to load.

New host: hosts/<name>/default.nix with nixosSystem listing *Configuration (+ *Home for desktops), configuration.nix exporting *Configuration, hardware + _private/ as needed. Desktop/laptop hosts also get home.nix exporting *Home (import self.nixosModules.desktopHomeBase unless it's a special case). Register in modules/deploy/navi.nix if it should be in the fleet.

When editing

  • nix flake check after touching modules/.
  • Stage/commit new files if the flake is read from git.
  • Small diffs; mkEnableOption + mkIf for toggles.
  • Cross-module deps: assertions or mkDefault, mention it in the option description.
  • New flake inputs in flake.nix with follows where versions should track nixpkgs.
  • client-services SSH defaults are for my LAN — don't copy to anything internet-facing without tightening.
  • If I'll forget the behavior in six months, update these docs in the same change.