diff --git a/lib/default.nix b/lib/default.nix index caba6a68..399bec91 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -512,6 +512,66 @@ rec { } ) users; + mkTerminal-refactor = + { + name, + vpnProfiles, + modules, + edition ? args.edition, + compression ? "zstd -Xcompression-level 6", + postInstallScript ? "", + }: + let + allModules = [ + ../modules + ../hardware + (import sources.lanzaboote).nixosModules.lanzaboote + "${sources.disko}/module.nix" + "${sources.agenix}/modules/age.nix" + { + securix.self.machine.identifier = name; + securix.self.edition = edition; + _module.args.vpnProfiles = vpnProfiles; + age.identityPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + } + ] ++ modules; + in + { + modules = allModules; + partitioningModules = [ + "${sources.disko}/module.nix" + ../modules/filesystems + ../modules/self.nix + ../modules/pam + ]; + installer = buildUSBInstallerISO { + modules = allModules; + inherit compression postInstallScript; + }; + system = pkgs.nixos allModules; + }; + + mkTerminals-refactor = + { + users, + vpn-profiles, + edition ? args.edition, + compression ? "zstd -Xcompression-level 6", + }: + baseSystem: + mapAttrs ( + name: userModule: + mkTerminal-refactor { + inherit name edition compression; + vpnProfiles = vpn-profiles; + modules = [ + baseSystem + userModule + ]; + } + ) users; + + # Build all documentation outputs for the Securix OS. mkDocs = { diff --git a/modules/default.nix b/modules/default.nix index ff096509..fd17cda2 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -54,6 +54,8 @@ ./i18n # Options related to the boot screen, UI, etc. ./boot + # Inventory related modules (machines and users) + ./inventory # React to NetworkManager events (low-level API) # Used to turn on proxies in response to certain VPNs lifecycles. ./networkmanager-events.nix diff --git a/modules/inventory/default.nix b/modules/inventory/default.nix new file mode 100644 index 00000000..7ac98676 --- /dev/null +++ b/modules/inventory/default.nix @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Mattias Kockum +# +# SPDX-License-Identifier: MIT + +{ imports = [ ./users.nix ./machines.nix ]; } diff --git a/modules/inventory/machines.nix b/modules/inventory/machines.nix new file mode 100644 index 00000000..c20322bf --- /dev/null +++ b/modules/inventory/machines.nix @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2026 Mattias Kockum +# +# SPDX-License-Identifier: MIT + +{ lib, config, ... }: +{ + options.securix.inventory.machine = lib.mkOption { + default = { }; + type = lib.types.submodule { + options = { + hardwareSKU = lib.mkOption { type = lib.types.str; default = ""; }; + serialNumber = lib.mkOption { type = lib.types.str; default = ""; }; + mainDisk = lib.mkOption { type = lib.types.str; default = ""; }; + users = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + }; + }; + + config = { + securix.self.mainDisk = lib.mkDefault config.securix.inventory.machine.mainDisk; + securix.self.machine.hardwareSKU = lib.mkDefault config.securix.inventory.machine.hardwareSKU; + securix.self.machine.serialNumber = lib.mkDefault config.securix.inventory.machine.serialNumber; + + assertions = map (username: { + assertion = config.securix.inventory.users ? ${username}; + message = "Machine '${config.securix.inventory.machine.serialNumber}' " + + "references user '${username}' who does not exist in " + + "securix.inventory.users."; + }) config.securix.inventory.machine.users; + }; +} diff --git a/modules/inventory/users.nix b/modules/inventory/users.nix new file mode 100644 index 00000000..8f685af8 --- /dev/null +++ b/modules/inventory/users.nix @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Mattias Kockum +# +# SPDX-License-Identifier: MIT + +{ lib, config, ... }: +let + userSubmodule = { name, ... }: { + options = { + email = lib.mkOption { type = lib.types.str; }; + username = lib.mkOption { type = lib.types.str; default = name; }; + hashedPassword = lib.mkOption { type = lib.types.str; }; + shell = lib.mkOption { type = lib.types.package; }; + isOperator = lib.mkOption { type = lib.types.bool; default = false; }; + }; + }; +in +{ + options.securix.inventory.users = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule userSubmodule); + default = { }; + }; + + config = { + users.users = lib.mapAttrs (_: user: { + isNormalUser = true; + hashedPassword = user.hashedPassword; + shell = user.shell; + }) (lib.filterAttrs + (name: _: builtins.elem name config.securix.inventory.machine.users) + config.securix.inventory.users + ); + }; +} diff --git a/tests/default.nix b/tests/default.nix index bc9fd46e..00accac9 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -7,4 +7,6 @@ minimal = import ./minimal.nix { inherit pkgs libSecurix; }; anssi-minimal = import ./anssi-minimal.nix { inherit pkgs libSecurix; }; idempotent-autoinstall = import ./idempotent-autoinstall.nix { inherit pkgs libSecurix; }; + inventory2 = import ./inventory2.nix { inherit pkgs libSecurix; }; + inventory-refactor = import ./inventory-refactor.nix { inherit pkgs libSecurix; }; } diff --git a/tests/inventory-refactor.nix b/tests/inventory-refactor.nix new file mode 100644 index 00000000..3c71ed95 --- /dev/null +++ b/tests/inventory-refactor.nix @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2026 Mattias Kockum +# +# SPDX-License-Identifier: MIT +# +# Integration test for the refactored inventory system. +# Verifies that a user declared via securix.inventory.users ends up +# correctly provisioned on the resulting NixOS system, without readInventory2. + +{ pkgs, libSecurix }: +let + + terminal = libSecurix.mkTerminal-refactor { + name = "ABC123"; + vpnProfiles = { }; + modules = [ + { + securix.inventory.machine = { + hardwareSKU = "x280"; + serialNumber = "ABC123"; + mainDisk = "/dev/nvme0n1"; + users = [ "alice" ]; + }; + securix.graphical-interface.variant = "sway"; + } + ({ pkgs, ... }: { + securix.inventory.users.alice = { + email = "alice@example.com"; + hashedPassword = "$y$j9T$zk4xGLyshz7RzqnMX6M8O0$AybRelILMkQSWcQZV4s.ykRNi/UlgaCUaDwdee0n7N2"; + shell = pkgs.bash; + }; + }) + ]; + }; + +in +pkgs.testers.nixosTest { + name = "inventory-refactor"; + nodes = { + securix-unbranded-ABC123 = { + imports = terminal.modules; + }; + }; + testScript = '' + securix_unbranded_ABC123.wait_for_unit("default.target") + securix_unbranded_ABC123.succeed("id alice") + securix_unbranded_ABC123.succeed("getent passwd alice | grep -q alice") + ''; +} diff --git a/tests/inventory2.nix b/tests/inventory2.nix new file mode 100644 index 00000000..bbb71db6 --- /dev/null +++ b/tests/inventory2.nix @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2026 Mattias Kockum +# +# SPDX-License-Identifier: MIT +# +# Integration test for readInventory2 + mkTerminal. +# Verifies that a user declared in an inventory2 directory ends up +# correctly provisioned on the resulting NixOS system. + +{ pkgs, libSecurix }: +let + fakeInventory = pkgs.runCommand "fake-inventory" { } '' + mkdir -p $out/machines $out/users + + cat > $out/machines/ABC123.nix <<'EOF' + { + securix.self = { + mainDisk = "/dev/nvme0n1"; + machine = { + hardwareSKU = "x280"; + serialNumber = "ABC123"; + users = [ "alice" ]; + }; + }; + } + EOF + + cat > $out/users/alice.nix <<'EOF' + { pkgs, ... }: + { + securix.self.user = { + email = "alice@example.com"; + username = "alice"; + hashedPassword = "$y$j9T$zk4xGLyshz7RzqnMX6M8O0$AybRelILMkQSWcQZV4s.ykRNi/UlgaCUaDwdee0n7N2"; + defaultLoginShell = pkgs.bash; + }; + } + EOF + ''; + + inventory = libSecurix.readInventory2 { dir = fakeInventory; }; + + terminal = libSecurix.mkTerminal { + name = "ABC123"; + userSpecificModule = { }; + vpnProfiles = { }; + modules = [ + inventory."ABC123".machineModule + { + securix.graphical-interface.variant = "sway"; + } + ] ++ inventory."ABC123".userModules; + }; +in +pkgs.testers.nixosTest { + name = "inventory2"; + nodes = { + securix-unbranded-ABC123 = { + imports = terminal.modules; + }; + }; + testScript = '' + securix_unbranded_ABC123.wait_for_unit("default.target") + securix_unbranded_ABC123.succeed("id alice") + securix_unbranded_ABC123.succeed("getent passwd alice | grep -q alice") + ''; +}