diff --git a/AGENTS.md b/AGENTS.md index efdcb78..66bf32e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,7 @@ One flake builds every machine I own. - Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths. For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths. + Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts. - `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads only top-level host `config.registry` declarations. Registry declarations inside guest interiors live under `config.containers..config.registry`. The collected registry does not include those guest-interior declarations. diff --git a/base.nix b/base.nix deleted file mode 100644 index b5d2a51..0000000 --- a/base.nix +++ /dev/null @@ -1,91 +0,0 @@ -{ - 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" - "storage" - ]; - }; - - # The shared write group. - # Its gid is fixed, so a host and every guest carry the same number. - # An identity-mapped container write then lands on the pool as this group, sparing every service the permission juggling. - # 10000 clears the system-group ids assigned automatically and leaves headroom above the primary user, so nothing else claims it. - users.groups.storage.gid = 10000; - - # 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 deleted file mode 100644 index 556a835..0000000 --- a/guest.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - 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. - my.registry.module - 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/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index bb25b69..b73669a 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -40,8 +40,6 @@ modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml; modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml; - 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`. # Modest caps keep the skeleton guest from starving the laptop. diff --git a/hosts/pikachu/default.nix b/hosts/pikachu/default.nix index 77edd8e..724de55 100644 --- a/hosts/pikachu/default.nix +++ b/hosts/pikachu/default.nix @@ -79,8 +79,6 @@ modules.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml; modules.git.enable = true; - modules.toolkit.enable = true; - guests.actualbudget = { enable = true; vlan = 20; diff --git a/lib/default.nix b/lib/default.nix index 94b1e88..7826de4 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -32,6 +32,9 @@ let ) (builtins.readDir dir) ); + moduleFiles = collectNixFiles (self + "/modules"); + guestFiles = collectNixFiles (self + "/guests"); + specialArgs = { inherit inputs self; my = self.lib; @@ -42,10 +45,13 @@ let guests = import ./guests.nix { inherit + inputs lib self specialArgs bridgeName + moduleFiles + registry ; }; @@ -57,17 +63,19 @@ let inputs.nixpkgs.lib.nixosSystem { inherit system specialArgs; modules = - (collectNixFiles (self + "/modules")) - ++ (collectNixFiles (self + "/guests")) + moduleFiles + ++ guestFiles ++ [ inputs.chaotic.nixosModules.default inputs.disko.nixosModules.disko inputs.sops-nix.nixosModules.sops inputs.stylix.nixosModules.stylix registry.module - (self + "/system.nix") (self + "/hosts/${hostName}") - { networking.hostName = hostName; } + { + networking.hostName = hostName; + modules.host.enable = true; + } ]; }; diff --git a/lib/guests.nix b/lib/guests.nix index 1c1a0fe..ba1a78f 100644 --- a/lib/guests.nix +++ b/lib/guests.nix @@ -1,8 +1,11 @@ { + inputs, lib, self, specialArgs, bridgeName, + moduleFiles, + registry, }: let inherit (lib) escapeShellArg mapAttrsToList; @@ -345,11 +348,18 @@ let inherit specialArgs; config = { - imports = [ - (self + "/guest.nix") - guestNet - interior - ]; + imports = + moduleFiles + ++ [ + registry.module + inputs.sops-nix.nixosModules.sops + inputs.stylix.nixosModules.stylix + { + modules.guest.enable = true; + } + guestNet + interior + ]; }; }; }; diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..d5910ba --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,126 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: +# The shared foundation for every host and guest. +let + inherit (lib) mkOption types; + user = config.user; + + 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 = '' + Primary interactive user for this system. + This drives the system account and Home Manager user together. + ''; + }; + description = mkOption { + type = types.str; + default = "Alexion"; + description = "Human-readable description for the primary user."; + }; + }; + + config = { + nixpkgs.overlays = [ + (_final: prev: { + unstable = import inputs.nixpkgs-unstable (pinArgs prev); + stable = import inputs.nixpkgs-stable (pinArgs prev); + }) + ]; + nixpkgs.config.allowUnfree = true; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + + users.users.${user.name} = { + isNormalUser = true; + description = user.description; + extraGroups = [ + "wheel" + "storage" + ]; + }; + + users.groups.storage.gid = 10000; + + 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"; + home.packages = with pkgs; [ + eza + wget + ]; + }; + }; + + registry.shell-shortcut.${config.networking.hostName} = { + ls = { + command = "eza -alg --color=always --group-directories-first --icons=always"; + abbreviation = false; + }; + la = { + command = "eza -a --color=always --group-directories-first --icons=always"; + abbreviation = false; + }; + ll = { + command = "eza -lg --color=always --group-directories-first --icons=always"; + abbreviation = false; + }; + lt = { + command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always"; + abbreviation = false; + }; + "l." = { + command = "eza -a | grep -e '^\\.'"; + abbreviation = false; + }; + + ".." = "cd .."; + "..." = "cd ../.."; + "...." = "cd ../../.."; + + cp = "cp -v"; + tarnow = "tar -acf "; + untar = "tar -zxvf "; + wget = "wget -c "; + grep = "grep --color=auto"; + fgrep = "fgrep --color=auto"; + egrep = "egrep --color=auto"; + jctl = "journalctl -p 3 -xb"; + + update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}"; + cleanup = "sudo nix-collect-garbage -d"; + }; + + modules.fish.enable = lib.mkDefault true; + modules.fish.defaultShell = lib.mkDefault true; + modules.tmux.enable = lib.mkDefault true; + modules.nvim.enable = lib.mkDefault true; + modules.git.enable = lib.mkDefault true; + modules.direnv.enable = lib.mkDefault true; + }; +} diff --git a/modules/guest.nix b/modules/guest.nix new file mode 100644 index 0000000..57b1070 --- /dev/null +++ b/modules/guest.nix @@ -0,0 +1,14 @@ +{ config, lib, ... }: +let + cfg = config.modules.guest; +in +{ + options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration"; + + config = lib.mkIf cfg.enable { + system.stateVersion = "26.05"; + + modules.ssh.enable = lib.mkDefault true; + modules.ssh.hostKeys.restore = lib.mkDefault false; + }; +} diff --git a/modules/host.nix b/modules/host.nix new file mode 100644 index 0000000..993702c --- /dev/null +++ b/modules/host.nix @@ -0,0 +1,42 @@ +{ + config, + lib, + ... +}: +let + cfg = config.modules.host; + user = config.user; + passwordSecret = "${user.name}-password"; +in +{ + options.modules.host.enable = lib.mkEnableOption "physical-host base configuration"; + + config = lib.mkIf cfg.enable { + nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ]; + nix.settings.extra-trusted-public-keys = [ + "nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk=" + ]; + + nix.gc.automatic = true; + nix.gc.dates = "Mon 03:15"; + nix.gc.options = "--delete-older-than 30d"; + + nix.optimise.automatic = true; + nix.optimise.dates = [ "Thu 03:45" ]; + + boot.loader.systemd-boot.configurationLimit = 15; + + services.xserver.xkb.layout = "us"; + services.xserver.xkb.options = "caps:escape_shifted_capslock"; + console.useXkbConfig = true; + + sops.defaultSopsFile = ../secrets/shared.yaml; + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + sops.age.sshKeyPaths = [ ]; + sops.gnupg.sshKeyPaths = [ ]; + + users.mutableUsers = false; + sops.secrets.${passwordSecret}.neededForUsers = true; + users.users.${user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path; + }; +} diff --git a/modules/toolkit.nix b/modules/toolkit.nix deleted file mode 100644 index 564d765..0000000 --- a/modules/toolkit.nix +++ /dev/null @@ -1,72 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -# The baseline interactive toolset as one deliberate bundle. -let - cfg = config.modules.toolkit; - user = config.user.name; -in -{ - options.modules.toolkit.enable = - lib.mkEnableOption "the baseline interactive environment — fish, tmux, nvim, git, and direnv — as one unit"; - - # Each piece is turned on at default priority, so a host can still override any one of them. - config = lib.mkIf cfg.enable { - home-manager.users.${user}.home.packages = with pkgs; [ - eza - wget - ]; - - registry.shell-shortcut.${config.networking.hostName} = { - ls = { - command = "eza -alg --color=always --group-directories-first --icons=always"; - abbreviation = false; - }; - la = { - command = "eza -a --color=always --group-directories-first --icons=always"; - abbreviation = false; - }; - ll = { - command = "eza -lg --color=always --group-directories-first --icons=always"; - abbreviation = false; - }; - lt = { - command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always"; - abbreviation = false; - }; - "l." = { - command = "eza -a | grep -e '^\\.'"; - abbreviation = false; - }; - - # Walk up the tree. - ".." = "cd .."; - "..." = "cd ../.."; - "...." = "cd ../../.."; - - cp = "cp -v"; - tarnow = "tar -acf "; - untar = "tar -zxvf "; - wget = "wget -c "; - grep = "grep --color=auto"; - fgrep = "fgrep --color=auto"; - egrep = "egrep --color=auto"; - jctl = "journalctl -p 3 -xb"; - - # Rebuild the system, and reclaim disk from old generations. - update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}"; - cleanup = "sudo nix-collect-garbage -d"; - }; - - modules.fish.enable = lib.mkDefault true; - # fish is also the login shell, so the bundled environment is what a shell drops into. - modules.fish.defaultShell = lib.mkDefault true; - modules.tmux.enable = lib.mkDefault true; - modules.nvim.enable = lib.mkDefault true; - modules.git.enable = lib.mkDefault true; - modules.direnv.enable = lib.mkDefault true; - }; -} diff --git a/system.nix b/system.nix deleted file mode 100644 index 8c23708..0000000 --- a/system.nix +++ /dev/null @@ -1,75 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -# 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 - user = config.user; - - passwordSecret = "${user.name}-password"; -in -{ - imports = [ ./base.nix ]; - - # 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=" - ]; - - # 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"; - - # 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" ]; - - # 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; - - environment.systemPackages = [ pkgs.git ]; - - # 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"; - - # 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; - - # 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 = [ ]; - - # 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; - - users.users.${user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path; -}