Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
{
Expand Down
2 changes: 2 additions & 0 deletions modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions modules/inventory/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2026 Mattias Kockum <mattias@kockum.net>
#
# SPDX-License-Identifier: MIT

{ imports = [ ./users.nix ./machines.nix ]; }
34 changes: 34 additions & 0 deletions modules/inventory/machines.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2026 Mattias Kockum <mattias@kockum.net>
#
# 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;
};
}
33 changes: 33 additions & 0 deletions modules/inventory/users.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2026 Mattias Kockum <mattias@kockum.net>
#
# 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
);
};
}
2 changes: 2 additions & 0 deletions tests/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
}
48 changes: 48 additions & 0 deletions tests/inventory-refactor.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# SPDX-FileCopyrightText: 2026 Mattias Kockum <mattias@kockum.net>
#
# 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")
'';
}
66 changes: 66 additions & 0 deletions tests/inventory2.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SPDX-FileCopyrightText: 2026 Mattias Kockum <mattias@kockum.net>
#
# 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")
'';
}
Loading