From e7d7eb14e105ffb1c3243c5845d6263d32c6c69b Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 25 Jul 2026 13:19:07 -0400 Subject: [PATCH] feat: introduce guests as nested-container definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a third auto-loaded kind beside the host and the module: the guest, a reusable definition under guests/ that a host enables like a module and that realizes its interior as a systemd-nspawn nested container. Split the shared base config so a guest can stand on it. base.nix now holds the substrate both bases share — the primary user, home-manager, and the unstable/stable overlays. system.nix keeps the host-only machinery, and a new guest.nix is the slim guest-base: it imports the full modules tree, pins the interior release, and auto-enables the toolkit bundle and SSH so any guest is workable on sight. Give modules.ssh a guest flavor. A host restores its host keys from secrets as before, while a guest sets hostKeys.restore = false, names no sops files, and self-generates a host key, so it holds no age key of its own. The lib grows a guest helper that declares the guests. namespace with an enable and a backend field. Only the container backend is built; microvm is a reserved value that trips a clear build-time assertion rather than silently building nothing. A sample guest exercises the whole path, and neogaia enables it, so the guest interior builds through the existing nix flake check seam. --- .claude/tasks/0002-guest-walking-skeleton.md | 54 ++++++ base.nix | 82 +++++++++ guest.nix | 31 ++++ guests/sample.nix | 5 + hosts/neogaia/default.nix | 4 + lib.nix | 101 ++++++++++-- modules/ssh.nix | 102 ++++++++---- system.nix | 165 ++++++------------- 8 files changed, 378 insertions(+), 166 deletions(-) create mode 100644 .claude/tasks/0002-guest-walking-skeleton.md create mode 100644 base.nix create mode 100644 guest.nix create mode 100644 guests/sample.nix diff --git a/.claude/tasks/0002-guest-walking-skeleton.md b/.claude/tasks/0002-guest-walking-skeleton.md new file mode 100644 index 0000000..ee1f655 --- /dev/null +++ b/.claude/tasks/0002-guest-walking-skeleton.md @@ -0,0 +1,54 @@ +--- +spec: guests +blocked-by: 0001-toolkit-bundle +--- + +## What to build + +The tracer bullet for the Guest concept: the thinnest complete path from discovery to a running nested container. +The Auto-loader gains a third kind, discovering every Guest under `guests/` the way it already discovers Modules and Hosts. +The shared base config splits into a host base (`system.nix`, carrying host-only machinery — bootloader, hardware profile, host identity, boot and garbage-collection timers) and a slim guest-base that every nested Guest stands on. +Both keep the primary user, home-manager, and the shared overlays; the guest-base additionally auto-enables the `toolkit` bundle and `modules.ssh`, and imports the full `modules/` tree so any Module is available inside a Guest. +A minimal sample Guest is Module-shaped: it declares its own `guests.` namespace with an `enable` and a `backend` field (default `container`, `microvm` reserved but not built), and guards its body per the Enable convention. +Its body's payload — the one difference from a Module — realizes a nested container running the Guest's interior on the guest-base. +A Host enables the sample Guest exactly as it enables a Module, and the whole thing builds through the existing `nix flake check` seam. + +## Acceptance criteria + +- [x] The Auto-loader discovers and wires every Guest under `guests/` as a third kind, with no manual `imports` edits, and a Guest's option path mirrors its `guests/` location per the Namespace convention (including the index-node and file-or-folder rules). +- [x] The shared base config is split into a host base and a slim guest-base; the existing Host still builds via `nix flake check` with its host-only machinery intact. +- [x] The guest-base includes the primary user, home-manager, and the shared overlays, auto-enables `toolkit` and `modules.ssh`, and imports the full `modules/` tree. +- [x] A sample Guest declares `guests..enable` plus a `backend` field defaulting to `container`, guards its body on `enable`, and realizes a nested container running its interior when a Host enables it. +- [x] The `microvm` backend value is accepted as reserved but unimplemented, failing clearly rather than silently building nothing. +- [x] A Host enabling the sample Guest builds via `nix flake check`, the guest is reachable from its Host by `machinectl` with no per-Guest configuration, and the baseline toolset and SSH access are present inside it. + +## Implementation Notes + +- The base split is realized as three files, not two. +`base.nix` is the shared substrate both bases build on — the primary user, home-manager, the `unstable`/`stable` overlays, and flakes. +`system.nix` (the host base) imports it and adds the host-only machinery (bootloader limit, sops decryption and the password, the maintenance timers, the chaotic cache, console keymap). +`guest.nix` (the guest-base) imports it and adds the slim guest layer. +Factoring the common substrate out keeps "both include the primary user, home-manager, and the shared overlays" a single fact rather than a duplicated one. + +- The guest-base imports `sops-nix` and `stylix` alongside the full `modules/` tree. +This is load-bearing, not incidental: the module system pushes an `mkIf` down to the leaves it guards, so an option path a module names must be *declared* even where its `enable` is off. +`modules/ssh.nix` names `sops.*` and the desktop modules name `stylix.*`, so those option namespaces have to exist for the tree to evaluate inside a guest that leaves them disabled. + +- `modules/ssh.nix` gained a guest flavor. +`hostKeys.restore` (default on) gates restoring host keys from secrets, and both `hostKeys.sopsFile` and `userKey.sopsFile` are now nullable. +A guest sets `restore = false` and names no sops files, so its daemon self-generates a host key and it carries no age key — verified: the interior's `sops.secrets` is empty and `services.openssh.hostKeys` falls back to the generated defaults, while `neogaia` still restores its committed host keys with `openssh.hostKeys = [ ]`. + +- The namespace mirroring is honored by author discipline, exactly as a Module's is: `my.guest { name = "sample"; }` names the option path, and `guests/sample.nix` places it. +The Auto-loader change is the same recursive `collectNixFiles`, so the index-node and file-or-folder rules a folder-shaped guest would use come for free from the loader that already serves Modules — no folder-shaped guest exists yet to exercise them. + +- The guest gets `privateNetwork = true` by default, so its interior sshd never contends with the host's on the shared namespace. +It is `mkDefault`, so the networking foundation can later attach the guest to a VLAN bridge. + +- `backend` is an `enum [ "container" "microvm" ]` defaulting to `container`. +`microvm` is built by nothing; choosing it trips a build-time assertion with a clear message rather than silently producing no container, per the acceptance criterion and ADR 0006's reserved-value decision. + +- `neogaia` enables `guests.sample` to demonstrate the path end to end, the way it demonstrated `modules.toolkit`. +This means a rebuild starts a live nspawn container on the laptop; it is a minimal smoke test and can be turned off with one line. + +- `nix flake check` passes and builds the nested `nixos-system-sample` interior in full. +A pre-existing nixvim warning about its own `nixpkgs.follows` now also prints for the guest's reused Neovim config; it is upstream noise, not a defect in this change. diff --git a/base.nix b/base.nix new file mode 100644 index 0000000..6c02160 --- /dev/null +++ b/base.nix @@ -0,0 +1,82 @@ +{ + config, + lib, + inputs, + ... +}: +# The shared foundation both the host base and the guest-base build on: the +# primary user, home-manager, and the fresher/pinned package overlays. +let + inherit (lib) mkOption types; + user = config.user; + + # Args to instantiate an extra nixpkgs source on the base platform. + pinArgs = prev: { + inherit (prev.stdenv.hostPlatform) system; + config.allowUnfree = true; + }; +in +{ + imports = [ inputs.home-manager.nixosModules.home-manager ]; + + options.user = { + name = mkOption { + type = types.str; + default = "alexion"; + description = '' + The primary interactive user this system is built for. Drives both the + system account and the home-manager user in lockstep. + ''; + }; + description = mkOption { + type = types.str; + default = "Alexion"; + description = "Human-readable description (GECOS field) for the primary user."; + }; + }; + + config = { + # Reach fresher packages with `unstable.` or pin with `stable.`. + nixpkgs.overlays = [ + (_final: prev: { + unstable = import inputs.nixpkgs-unstable (pinArgs prev); + stable = import inputs.nixpkgs-stable (pinArgs prev); + }) + ]; + nixpkgs.config.allowUnfree = true; + + # Flakes, so `nixos-rebuild switch` works from the console and a direnv + # `use flake` resolves inside a guest. + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + + # Primary user. + # The wheel group is the way in, since root is locked. + # No password is set here, since that is host-only. + # A guest therefore has none and is reached by SSH key or `machinectl`. + users.users.${user.name} = { + isNormalUser = true; + description = user.description; + extraGroups = [ "wheel" ]; + }; + + # home-manager as a NixOS module: one build produces the system and user + # environment together, sharing the system's pkgs and installing user + # packages into the system profile. + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + extraSpecialArgs = { + inherit inputs; + my = inputs.self.lib; + }; + users.${user.name} = { + home.username = user.name; + home.homeDirectory = "/home/${user.name}"; + home.stateVersion = "26.05"; + }; + }; + }; +} diff --git a/guest.nix b/guest.nix new file mode 100644 index 0000000..aedba9b --- /dev/null +++ b/guest.nix @@ -0,0 +1,31 @@ +{ + my, + inputs, + lib, + ... +}: +# The guest-base: the slim foundation every nested guest's interior stands on. +# It imports the full modules tree so any module is available to enable inside a +# guest, and stands on the same shared base a host does. +{ + imports = my.collectNixFiles (inputs.self + "/modules") ++ [ + (inputs.self + "/base.nix") + + # The modules tree reaches for these option namespaces, so they must be + # declared for the tree to evaluate even where a guest leaves them off. + inputs.sops-nix.nixosModules.sops + inputs.stylix.nixosModules.stylix + ]; + + # A nested container has no per-host `default.nix` to pin its release. + system.stateVersion = "26.05"; + + # The baseline toolset and SSH access, so any guest shelled into is a workable + # environment without per-guest wiring. + modules.toolkit.enable = lib.mkDefault true; + modules.ssh.enable = lib.mkDefault true; + + # A guest carries no host identity, so it presents a self-generated host key + # rather than restoring one from secrets. + modules.ssh.hostKeys.restore = lib.mkDefault false; +} diff --git a/guests/sample.nix b/guests/sample.nix new file mode 100644 index 0000000..293621a --- /dev/null +++ b/guests/sample.nix @@ -0,0 +1,5 @@ +args@{ my, ... }: +# The tracer-bullet guest: the thinnest complete path from discovery to a +# running nested container. Its interior is just the guest-base — the baseline +# toolset and SSH access — so it proves the concept without carrying a service. +my.guest { name = "sample"; } args diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index 7fe552b..d91ea56 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -44,6 +44,10 @@ modules.toolkit.enable = true; + # The walking-skeleton guest, enabled like any module: proves the guest path + # end to end through this host's `nix flake check`. + guests.sample.enable = true; + modules.agents.claude-code.enable = true; modules.agents.tools.gitea-axi.enable = true; modules.agents.pi.enable = true; diff --git a/lib.nix b/lib.nix index b4a72be..52b5d78 100644 --- a/lib.nix +++ b/lib.nix @@ -32,30 +32,96 @@ let ) (builtins.readDir dir) ); - # Build one host: every module is imported unconditionally (inert until its - # `enable` flag is set), alongside home-manager, chaotic, the shared base, and - # the host's own directory. + # The special arguments every configuration is evaluated with, host and guest + # interior alike. + specialArgs = { + inherit inputs; + my = self.lib; + }; + + # Build one host: every module and every guest is imported unconditionally + # (inert until its `enable` flag is set), alongside chaotic, the host base, + # and the host's own directory. mkHost = { hostName, system ? "x86_64-linux", }: inputs.nixpkgs.lib.nixosSystem { - inherit system; - specialArgs = { - inherit inputs; - my = self.lib; + inherit system specialArgs; + modules = + (collectNixFiles (self + "/modules")) + ++ (collectNixFiles (self + "/guests")) + ++ [ + inputs.chaotic.nixosModules.default + inputs.disko.nixosModules.disko + inputs.sops-nix.nixosModules.sops + inputs.stylix.nixosModules.stylix + (self + "/system.nix") + (self + "/hosts/${hostName}") + { networking.hostName = hostName; } + ]; + }; + + # Build a guest: a module-shaped definition whose body realizes its interior + # as a nested container standing on the guest-base, keyed by its namespace path. + # `name` is the dotted namespace under `guests.` and `interior` is an extra + # module merged into the container alongside the guest-base. + guest = + { + name, + interior ? { }, + }: + { config, lib, ... }: + let + optionPath = [ "guests" ] ++ lib.splitString "." name; + cfg = lib.getAttrFromPath optionPath config; + machineName = lib.replaceStrings [ "." ] [ "-" ] name; + in + { + options = lib.setAttrByPath optionPath { + enable = lib.mkEnableOption "the ${name} guest, run in its own nested container"; + backend = lib.mkOption { + type = lib.types.enum [ + "container" + "microvm" + ]; + default = "container"; + description = '' + How the guest is realized. `container` runs the guest as a + systemd-nspawn nested container. `microvm` is reserved for a future + hard-isolation backend and is not built yet. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.backend == "container"; + message = '' + guests.${name}.backend = "${cfg.backend}" is not implemented. Only the "container" backend is built; "microvm" is reserved for future work. + ''; + } + ]; + + containers.${machineName} = lib.mkIf (cfg.backend == "container") { + autoStart = lib.mkDefault true; + + # The guest gets its own network namespace, so its services — its own + # sshd included — never contend with the host's. + privateNetwork = lib.mkDefault true; + + inherit specialArgs; + + config = { + imports = [ + (self + "/guest.nix") + interior + ]; + }; + }; }; - modules = (collectNixFiles (self + "/modules")) ++ [ - inputs.home-manager.nixosModules.home-manager - inputs.chaotic.nixosModules.default - inputs.disko.nixosModules.disko - inputs.sops-nix.nixosModules.sops - inputs.stylix.nixosModules.stylix - (self + "/system.nix") - (self + "/hosts/${hostName}") - { networking.hostName = hostName; } - ]; }; # Discover every host (a subdirectory of `hostsDir`) and build each one. @@ -71,5 +137,6 @@ in collectNixFiles mkHost mkHosts + guest ; } diff --git a/modules/ssh.nix b/modules/ssh.nix index 84e3734..8b819ba 100644 --- a/modules/ssh.nix +++ b/modules/ssh.nix @@ -53,11 +53,25 @@ in ''; }; + hostKeys.restore = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Restore the host keys from secrets rather than letting the daemon + generate its own. + + A machine with its own identity keeps its fingerprint across a reimage + by restoring committed keys. A guest carries no host identity, so it + turns this off and presents a self-generated key instead. + ''; + }; + hostKeys.sopsFile = lib.mkOption { - type = lib.types.path; + type = lib.types.nullOr lib.types.path; + default = null; description = '' Encrypted file holding this host's SSH host private keys, one entry per - key type, named `ssh-host--key`. + key type, named `ssh-host--key`. Required when `restore` is on. These are the keys the daemon presents to identify itself to connecting clients, not keys used to authenticate anyone to a remote server. @@ -81,10 +95,12 @@ in }; userKey.sopsFile = lib.mkOption { - type = lib.types.path; + type = lib.types.nullOr lib.types.path; + default = null; description = '' Encrypted file holding this machine's SSH client private key, under the - entry `ssh-user-ed25519-key`. + entry `ssh-user-ed25519-key`. Left unset on a machine that authenticates + to no remote server, such as a guest. This is the key the primary user offers to authenticate to a remote server, not a key the daemon presents to identify this machine. @@ -94,45 +110,61 @@ in }; }; - config = lib.mkIf cfg.enable { - services.openssh.enable = true; + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + services.openssh.enable = true; - sops.secrets = - # The daemon reads its host keys once at startup, so a re-key has to - # restart it to take effect. - lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: { - inherit (cfg.hostKeys) sopsFile; - mode = "0400"; - restartUnits = [ "sshd.service" ]; + # The primary user is the only account reachable over SSH. + users.users.${user}.openssh.authorizedKeys.keys = cfg.authorizedKeys; + } + + # A machine with its own identity restores its host keys from secrets. + (lib.mkIf cfg.hostKeys.restore { + assertions = [ + { + assertion = cfg.hostKeys.sopsFile != null; + message = "modules.ssh.hostKeys.restore requires modules.ssh.hostKeys.sopsFile to name the encrypted host keys."; + } + ]; + + # The daemon reads its host keys once at startup, so a re-key has to + # restart it to take effect. + sops.secrets = lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: { + inherit (cfg.hostKeys) sopsFile; + mode = "0400"; + restartUnits = [ "sshd.service" ]; + }); + + # An empty list is what stops the daemon generating keys of its own. + services.openssh.hostKeys = [ ]; + services.openssh.extraConfig = lib.concatMapStrings ( + type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n" + ) cfg.hostKeys.types; }) - // { + + # The client key the primary user offers to remote servers, present only on + # a machine that has one. + (lib.mkIf (cfg.userKey.sopsFile != null) { # The primary user is the only account that authenticates with this key, # and the mode admits no other. # The client rereads it per connection, so no unit restarts on a re-key. - ${userKeySecret} = { + sops.secrets.${userKeySecret} = { inherit (cfg.userKey) sopsFile; mode = "0400"; owner = user; }; - }; - # An empty list is what stops the daemon generating keys of its own. - services.openssh.hostKeys = [ ]; - services.openssh.extraConfig = lib.concatMapStrings ( - type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n" - ) cfg.hostKeys.types; - - # The primary user is the only account reachable over SSH. - users.users.${user}.openssh.authorizedKeys.keys = cfg.authorizedKeys; - - # The client reads the decrypted key where it is written, so no copy of it - # lives in the user's home to drift from the secret. - # Declaring no defaults of home-manager's own leaves every other directive - # at the one OpenSSH itself ships. - home-manager.users.${user}.programs.ssh = { - enable = true; - enableDefaultConfig = false; - settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path; - }; - }; + # The client reads the decrypted key where it is written, so no copy of it + # lives in the user's home to drift from the secret. + # Declaring no defaults of home-manager's own leaves every other directive + # at the one OpenSSH itself ships. + home-manager.users.${user}.programs.ssh = { + enable = true; + enableDefaultConfig = false; + settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path; + }; + }) + ] + ); } diff --git a/system.nix b/system.nix index 552b239..8c23708 100644 --- a/system.nix +++ b/system.nix @@ -2,137 +2,74 @@ config, lib, pkgs, - inputs, ... }: -# Shared base config carried by every host. +# The host base: the host-only machinery a physical machine needs on top of the +# shared base — bootloader, secret decryption, and the maintenance timers. let - inherit (lib) mkOption types; user = config.user; passwordSecret = "${user.name}-password"; - - # Args to instantiate an extra nixpkgs source on the base platform. - pinArgs = prev: { - inherit (prev.stdenv.hostPlatform) system; - config.allowUnfree = true; - }; in { - options.user = { - name = mkOption { - type = types.str; - default = "alexion"; - description = '' - The primary interactive user this host is built for. Drives both the - system account and the home-manager user in lockstep. - ''; - }; - description = mkOption { - type = types.str; - default = "Alexion"; - description = "Human-readable description (GECOS field) for the primary user."; - }; - }; + imports = [ ./base.nix ]; - config = { - # Reach fresher packages with `unstable.` or pin with `stable.`. - # chaotic's overlay is added by its own module, not here. - nixpkgs.overlays = [ - (_final: prev: { - unstable = import inputs.nixpkgs-unstable (pinArgs prev); - stable = import inputs.nixpkgs-stable (pinArgs prev); - }) - ]; - nixpkgs.config.allowUnfree = true; + # chaotic's binary cache, so the CachyOS kernel is fetched rather than compiled. + # The `extra-` prefix keeps cache.nixos.org alongside it. + nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ]; + nix.settings.extra-trusted-public-keys = [ + "nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk=" + ]; - # Flakes, so `nixos-rebuild switch` works from the console. - nix.settings.experimental-features = [ - "nix-command" - "flakes" - ]; + # A month of generations is kept, because on a rolling channel with a + # third-party kernel an old generation is a known-good system to boot when + # an update breaks something. + nix.gc.automatic = true; + nix.gc.dates = "Mon 03:15"; + nix.gc.options = "--delete-older-than 30d"; - # chaotic's binary cache, so the CachyOS kernel is fetched rather than compiled. - # The `extra-` prefix keeps cache.nixos.org alongside it. - nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ]; - nix.settings.extra-trusted-public-keys = [ - "nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk=" - ]; + # Deduplication runs on a timer, off the rebuild path, so it never adds + # latency to a `nixos-rebuild switch`. + # It falls on a different day from collection, so the two never contend. + nix.optimise.automatic = true; + nix.optimise.dates = [ "Thu 03:45" ]; - # A month of generations is kept, because on a rolling channel with a - # third-party kernel an old generation is a known-good system to boot when - # an update breaks something. - nix.gc.automatic = true; - nix.gc.dates = "Mon 03:15"; - nix.gc.options = "--delete-older-than 30d"; + # The EFI system partition holds a kernel and an initrd per entry at roughly + # 70 MiB apiece, and is fixed in size. + # An exhausted one fails at bootloader installation, after the build has + # already succeeded. + boot.loader.systemd-boot.configurationLimit = 15; - # Deduplication runs on a timer, off the rebuild path, so it never adds - # latency to a `nixos-rebuild switch`. - # It falls on a different day from collection, so the two never contend. - nix.optimise.automatic = true; - nix.optimise.dates = [ "Thu 03:45" ]; + environment.systemPackages = [ pkgs.git ]; - # The EFI system partition holds a kernel and an initrd per entry at roughly - # 70 MiB apiece, and is fixed in size. - # An exhausted one fails at bootloader installation, after the build has - # already succeeded. - boot.loader.systemd-boot.configurationLimit = 15; + # Caps Lock is a second Escape. + # Shift+Caps Lock still toggles Caps Lock. + services.xserver.xkb.layout = "us"; + services.xserver.xkb.options = "caps:escape_shifted_capslock"; - environment.systemPackages = [ pkgs.git ]; + # Compile the console keymap from the layout above, so the remap holds on a + # bare TTY and not only under a graphical session. + console.useXkbConfig = true; - # Caps Lock is a second Escape. - # Shift+Caps Lock still toggles Caps Lock. - services.xserver.xkb.layout = "us"; - services.xserver.xkb.options = "caps:escape_shifted_capslock"; + # Decryption machinery every host depends on. + # The identity sits on the encrypted root, which is mounted early enough to + # satisfy the secret below. + # Clearing both `sshKeyPaths` defaults keeps the SSH host keys out of the + # decryption path. + sops.defaultSopsFile = ./secrets/shared.yaml; + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + sops.age.sshKeyPaths = [ ]; + sops.gnupg.sshKeyPaths = [ ]; - # Compile the console keymap from the layout above, so the remap holds on a - # bare TTY and not only under a graphical session. - console.useXkbConfig = true; + # A password set by hand on a running machine otherwise takes precedence. + # That leaves the declared `hashedPasswordFile` below silently inert. + # Root has no declared password and is therefore locked. + # `sudo` from the wheel group is the way in. + users.mutableUsers = false; - # Decryption machinery every host depends on. - # The identity sits on the encrypted root, which is mounted early enough to - # satisfy the secret below. - # Clearing both `sshKeyPaths` defaults keeps the SSH host keys out of the - # decryption path. - sops.defaultSopsFile = ./secrets/shared.yaml; - sops.age.keyFile = "/var/lib/sops-nix/key.txt"; - sops.age.sshKeyPaths = [ ]; - sops.gnupg.sshKeyPaths = [ ]; + # Decrypted in an earlier activation stage than ordinary secrets. + # That is early enough to precede the account that reads it. + sops.secrets.${passwordSecret}.neededForUsers = true; - # A password set by hand on a running machine otherwise takes precedence. - # That leaves the declared `hashedPasswordFile` below silently inert. - # Root has no declared password and is therefore locked. - # `sudo` from the wheel group is the way in. - users.mutableUsers = false; - - # Decrypted in an earlier activation stage than ordinary secrets. - # That is early enough to precede the account that reads it. - sops.secrets.${passwordSecret}.neededForUsers = true; - - # Primary user. - # The wheel group is the way in, since root is locked. - users.users.${user.name} = { - isNormalUser = true; - description = user.description; - extraGroups = [ "wheel" ]; - hashedPasswordFile = config.sops.secrets.${passwordSecret}.path; - }; - - # home-manager as a NixOS module: one `nixos-rebuild switch` builds the - # system and user environment together, sharing the system's pkgs and - # installing user packages into the system profile. - home-manager = { - useGlobalPkgs = true; - useUserPackages = true; - extraSpecialArgs = { - inherit inputs; - my = inputs.self.lib; - }; - users.${user.name} = { - home.username = user.name; - home.homeDirectory = "/home/${user.name}"; - home.stateVersion = "26.05"; - }; - }; - }; + users.users.${user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path; } -- 2.47.3