diff --git a/nixos/modules/services/system/userborn.nix b/nixos/modules/services/system/userborn.nix index 73b7581bd2b7..f9595fe72654 100644 --- a/nixos/modules/services/system/userborn.nix +++ b/nixos/modules/services/system/userborn.nix @@ -35,6 +35,9 @@ let }; userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig); + userbornStaticFiles = + pkgs.runCommand "static-userborn" { } + "mkdir -p $out; ${lib.getExe cfg.package} ${userbornConfigJson} $out"; immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable; # The filenames created by userborn. @@ -51,12 +54,29 @@ in enable = lib.mkEnableOption "userborn"; + static = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to generate the password files at build time and store them directly + in the system closure, without requiring any services at boot time. + + This is STRICTLY intended for embedded appliance images that only have system + users with manually managed static user IDs, and CANNOT be used with generation + updates. + + WARNING: In this mode, you MUST statically manage user IDs yourself, carefully. + Beware, UID reuse is a serious security issue and it's your responsibility + to avoid it over the entire lifetime of the system. + ''; + }; + package = lib.mkPackageOption pkgs "userborn" { }; passwordFilesLocation = lib.mkOption { type = lib.types.str; - default = if immutableEtc then "/var/lib/nixos" else "/etc"; - defaultText = lib.literalExpression ''if immutableEtc then "/var/lib/nixos" else "/etc"''; + default = if immutableEtc && !cfg.static then "/var/lib/nixos" else "/etc"; + defaultText = lib.literalExpression ''if immutableEtc && !config.services.userborn.static then "/var/lib/nixos" else "/etc"''; description = '' The location of the original password files. @@ -83,8 +103,12 @@ in message = "system.activationScripts.users has to be empty to use userborn"; } { - assertion = immutableEtc -> (cfg.passwordFilesLocation != "/etc"); - message = "When `system.etc.overlay.mutable = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`"; + assertion = (immutableEtc && !cfg.static) -> (cfg.passwordFilesLocation != "/etc"); + message = "When `system.etc.overlay.mutable = false` and `services.userborn.static = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`"; + } + { + assertion = !(cfg.static && config.system.switch.enable); + message = "You cannot use `services.userborn.static = true` with switchable configurations, it is ONLY indended for appliance images with fully static user IDs"; } ]; @@ -110,7 +134,7 @@ in ) userCfg.users ); - services.userborn = { + services.userborn = lib.mkIf (!cfg.static) { wantedBy = [ "sysinit.target" ]; requiredBy = [ "sysinit-reactivation.target" ]; after = [ @@ -166,20 +190,35 @@ in }; }; - # Statically create the symlinks to passwordFilesLocation when they're not - # inside /etc because we will not be able to do it at runtime in case of an - # immutable /etc! - environment.etc = lib.mkIf (cfg.passwordFilesLocation != "/etc") ( - lib.listToAttrs ( - lib.map ( - file: - lib.nameValuePair file { - source = "${cfg.passwordFilesLocation}/${file}"; - mode = "direct-symlink"; - } - ) passwordFiles - ) - ); + environment.etc = lib.mkMerge [ + (lib.mkIf cfg.static ( + # In static mode, statically drop the files into an immutable /etc. + lib.listToAttrs ( + lib.map ( + file: + lib.nameValuePair file { + source = "${userbornStaticFiles}/${file}"; + mode = if file == "shadow" then "0000" else "0644"; + } + ) passwordFiles + ) + )) + + (lib.mkIf (!cfg.static && cfg.passwordFilesLocation != "/etc") ( + # Statically create the symlinks to passwordFilesLocation when they're not + # inside /etc because we will not be able to do it at runtime in case of a + # (non-static) immutable /etc! + lib.listToAttrs ( + lib.map ( + file: + lib.nameValuePair file { + source = "${cfg.passwordFilesLocation}/${file}"; + mode = "direct-symlink"; + } + ) passwordFiles + ) + )) + ]; }; meta.maintainers = with lib.maintainers; [ nikstur ]; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ddd09ceb28d2..e845597fa3ef 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1647,6 +1647,7 @@ in userborn-immutable-users = runTest ./userborn-immutable-users.nix; userborn-mutable-etc = runTest ./userborn-mutable-etc.nix; userborn-mutable-users = runTest ./userborn-mutable-users.nix; + userborn-static = runTest ./userborn-static.nix; ustreamer = runTest ./ustreamer.nix; uwsgi = runTest ./uwsgi.nix; v2ray = runTest ./v2ray.nix; diff --git a/nixos/tests/userborn-static.nix b/nixos/tests/userborn-static.nix new file mode 100644 index 000000000000..e1d5cf259a7d --- /dev/null +++ b/nixos/tests/userborn-static.nix @@ -0,0 +1,57 @@ +{ lib, ... }: + +let + sysuserPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639"; + + common = { + services.userborn = { + enable = true; + static = true; + }; + boot.initrd.systemd.enable = true; + networking.useNetworkd = true; + system.etc.overlay = { + enable = true; + mutable = false; + }; + }; +in + +{ + + name = "userborn-static"; + + meta.maintainers = with lib.maintainers; [ + mic92 + valpackett + ]; + + nodes.machine = + { ... }: + { + imports = [ common ]; + + users.users.sysuser = { + uid = 1337; + isSystemUser = true; + group = "wheel"; + home = "/var/empty"; + initialHashedPassword = sysuserPassword; + }; + }; + + testScript = '' + with subtest("Correct mode on the password files"): + assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n" + + with subtest("Check files"): + print(machine.succeed("grpck -r")) + print(machine.succeed("pwck -r")) + + with subtest("sysuser user is created"): + print(machine.succeed("getent passwd sysuser")) + assert "${sysuserPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not correct" + ''; +}