diff --git a/nixos/modules/system/boot/systemd/sysusers.nix b/nixos/modules/system/boot/systemd/sysusers.nix index e81fa33f66e5..bb49fe9650c3 100644 --- a/nixos/modules/system/boot/systemd/sysusers.nix +++ b/nixos/modules/system/boot/systemd/sysusers.nix @@ -32,32 +32,12 @@ let } ''; - staticSysusersCredentials = pkgs.runCommand "static-sysusers-credentials" { } '' - mkdir $out; cd $out - ${lib.concatLines ( - (lib.mapAttrsToList - (username: opts: "echo -n '${opts.initialHashedPassword}' > 'passwd.hashed-password.${username}'") - (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers)) - ++ - (lib.mapAttrsToList - (username: opts: "echo -n '${opts.initialPassword}' > 'passwd.plaintext-password.${username}'") - (lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers)) - ++ - (lib.mapAttrsToList - (username: opts: "cat '${opts.hashedPasswordFile}' > 'passwd.hashed-password.${username}'") - (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers)) - ) - } - ''; - - staticSysusers = pkgs.runCommand "static-sysusers" - { - nativeBuildInputs = [ pkgs.systemd ]; - } '' - mkdir $out - export CREDENTIALS_DIRECTORY=${staticSysusersCredentials} - systemd-sysusers --root $out ${sysusersConfig}/00-nixos.conf - ''; + immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable; + # The location of the password files when using an immutable /etc. + immutablePasswordFilesLocation = "/var/lib/nixos/etc"; + passwordFilesLocation = if immutableEtc then immutablePasswordFilesLocation else "/etc"; + # The filenames created by systemd-sysusers. + passwordFiles = [ "passwd" "group" "shadow" "gshadow" ]; in @@ -99,93 +79,100 @@ in }) userCfg.users; - systemd = lib.mkMerge [ - ({ + systemd = { - # Create home directories, do not create /var/empty even if that's a user's - # home. - tmpfiles.settings.home-directories = lib.mapAttrs' - (username: opts: lib.nameValuePair opts.home { - d = { - mode = opts.homeMode; - user = username; - group = opts.group; - }; - }) - (lib.filterAttrs (_username: opts: opts.home != "/var/empty") systemUsers); - - # Create uid/gid marker files for those without an explicit id - tmpfiles.settings.nixos-uid = lib.mapAttrs' - (username: opts: lib.nameValuePair "/var/lib/nixos/uid/${username}" { - f = { - user = username; - }; - }) - (lib.filterAttrs (_username: opts: opts.uid == null) systemUsers); - - tmpfiles.settings.nixos-gid = lib.mapAttrs' - (groupname: opts: lib.nameValuePair "/var/lib/nixos/gid/${groupname}" { - f = { - group = groupname; - }; - }) - (lib.filterAttrs (_groupname: opts: opts.gid == null) userCfg.groups); - }) - - (lib.mkIf config.users.mutableUsers { - additionalUpstreamSystemUnits = [ - "systemd-sysusers.service" - ]; - - services.systemd-sysusers = { - # Enable switch-to-configuration to restart the service. - unitConfig.ConditionNeedsUpdate = [ "" ]; - requiredBy = [ "sysinit-reactivation.target" ]; - before = [ "sysinit-reactivation.target" ]; - restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ]; - - serviceConfig = { - LoadCredential = lib.mapAttrsToList - (username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}") - (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers); - SetCredential = (lib.mapAttrsToList - (username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}") - (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers)) - ++ - (lib.mapAttrsToList - (username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}") - (lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers)) - ; + # Create home directories, do not create /var/empty even if that's a user's + # home. + tmpfiles.settings.home-directories = lib.mapAttrs' + (username: opts: lib.nameValuePair opts.home { + d = { + mode = opts.homeMode; + user = username; + group = opts.group; }; + }) + (lib.filterAttrs (_username: opts: opts.home != "/var/empty") systemUsers); + + # Create uid/gid marker files for those without an explicit id + tmpfiles.settings.nixos-uid = lib.mapAttrs' + (username: opts: lib.nameValuePair "/var/lib/nixos/uid/${username}" { + f = { + user = username; + }; + }) + (lib.filterAttrs (_username: opts: opts.uid == null) systemUsers); + + tmpfiles.settings.nixos-gid = lib.mapAttrs' + (groupname: opts: lib.nameValuePair "/var/lib/nixos/gid/${groupname}" { + f = { + group = groupname; + }; + }) + (lib.filterAttrs (_groupname: opts: opts.gid == null) userCfg.groups); + + additionalUpstreamSystemUnits = [ + "systemd-sysusers.service" + ]; + + services.systemd-sysusers = { + # Enable switch-to-configuration to restart the service. + unitConfig.ConditionNeedsUpdate = [ "" ]; + requiredBy = [ "sysinit-reactivation.target" ]; + before = [ "sysinit-reactivation.target" ]; + restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ]; + + serviceConfig = { + # When we have an immutable /etc we cannot write the files directly + # to /etc so we write it to a different directory and symlink them + # into /etc. + # + # We need to explicitly list the config file, otherwise + # systemd-sysusers cannot find it when we also pass another flag. + ExecStart = lib.mkIf immutableEtc + [ "" "${config.systemd.package}/bin/systemd-sysusers --root ${builtins.dirOf immutablePasswordFilesLocation} /etc/sysusers.d/00-nixos.conf" ]; + + # Make the source files writable before executing sysusers. + ExecStartPre = lib.mkIf (!userCfg.mutableUsers) + (lib.map + (file: "-${pkgs.util-linux}/bin/umount ${passwordFilesLocation}/${file}") + passwordFiles); + # Make the source files read-only after sysusers has finished. + ExecStartPost = lib.mkIf (!userCfg.mutableUsers) + (lib.map + (file: "${pkgs.util-linux}/bin/mount --bind -o ro ${passwordFilesLocation}/${file} ${passwordFilesLocation}/${file}") + passwordFiles); + + LoadCredential = lib.mapAttrsToList + (username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}") + (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers); + SetCredential = (lib.mapAttrsToList + (username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}") + (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers)) + ++ + (lib.mapAttrsToList + (username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}") + (lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers)) + ; }; - }) - ]; + }; + + }; environment.etc = lib.mkMerge [ - (lib.mkIf (!userCfg.mutableUsers) { - "passwd" = { - source = "${staticSysusers}/etc/passwd"; - mode = "0644"; - }; - "group" = { - source = "${staticSysusers}/etc/group"; - mode = "0644"; - }; - "shadow" = { - source = "${staticSysusers}/etc/shadow"; - mode = "0000"; - }; - "gshadow" = { - source = "${staticSysusers}/etc/gshadow"; - mode = "0000"; - }; - }) - - (lib.mkIf userCfg.mutableUsers { + ({ "sysusers.d".source = sysusersConfig; }) - ]; + # Statically create the symlinks to immutablePasswordFilesLocation when + # using an immutable /etc because we will not be able to do it at + # runtime! + (lib.mkIf immutableEtc (lib.listToAttrs (lib.map + (file: lib.nameValuePair file { + source = "${immutablePasswordFilesLocation}/${file}"; + mode = "direct-symlink"; + }) + passwordFiles))) + ]; }; meta.maintainers = with lib.maintainers; [ nikstur ]; diff --git a/nixos/tests/activation/etc-overlay-immutable.nix b/nixos/tests/activation/etc-overlay-immutable.nix index a28abe222320..dbb7337b5dc1 100644 --- a/nixos/tests/activation/etc-overlay-immutable.nix +++ b/nixos/tests/activation/etc-overlay-immutable.nix @@ -32,6 +32,18 @@ with subtest("direct symlinks point to the target without indirection"): assert machine.succeed("readlink -n /etc/localtime") == "/etc/zoneinfo/Utc" + with subtest("Correct mode on the source password files"): + assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/shadow") == "0\n" + assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/gshadow") == "0\n" + + with subtest("Password files are symlinks to /var/lib/nixos/etc"): + assert machine.succeed("readlink -f /etc/passwd") == "/var/lib/nixos/etc/passwd\n" + assert machine.succeed("readlink -f /etc/group") == "/var/lib/nixos/etc/group\n" + assert machine.succeed("readlink -f /etc/shadow") == "/var/lib/nixos/etc/shadow\n" + assert machine.succeed("readlink -f /etc/gshadow") == "/var/lib/nixos/etc/gshadow\n" + with subtest("switching to the same generation"): machine.succeed("/run/current-system/bin/switch-to-configuration test") diff --git a/nixos/tests/systemd-sysusers-immutable.nix b/nixos/tests/systemd-sysusers-immutable.nix index 1d76fa71b5b9..4d65b52a0d33 100644 --- a/nixos/tests/systemd-sysusers-immutable.nix +++ b/nixos/tests/systemd-sysusers-immutable.nix @@ -16,9 +16,12 @@ in systemd.sysusers.enable = true; users.mutableUsers = false; - # Override the empty root password set by the test instrumentation - users.users.root.hashedPasswordFile = lib.mkForce null; - users.users.root.initialHashedPassword = rootPassword; + + # Read this password file at runtime from outside the Nix store. + environment.etc."rootpw.secret".text = rootPassword; + # Override the empty root password set by the test instrumentation. + users.users.root.hashedPasswordFile = lib.mkForce "/etc/rootpw.secret"; + users.users.sysuser = { isSystemUser = true; group = "wheel"; @@ -37,16 +40,6 @@ in }; testScript = '' - with subtest("Users are not created with systemd-sysusers"): - machine.fail("systemctl status systemd-sysusers.service") - machine.fail("ls /etc/sysusers.d") - - 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" - assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n" - with subtest("root user has correct password"): print(machine.succeed("getent passwd root")) assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct" @@ -56,13 +49,15 @@ in assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n" assert "${sysuserPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not correct" + with subtest("Fail to add new user manually"): + machine.fail("useradd manual-sysuser") + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") with subtest("new-sysuser user is created after switching to new generation"): print(machine.succeed("getent passwd new-sysuser")) - print(machine.succeed("getent shadow new-sysuser")) assert machine.succeed("stat -c '%U' /new-sysuser") == "new-sysuser\n" ''; } diff --git a/nixos/tests/systemd-sysusers-mutable.nix b/nixos/tests/systemd-sysusers-mutable.nix index dd98013df976..9871a91cca97 100644 --- a/nixos/tests/systemd-sysusers-mutable.nix +++ b/nixos/tests/systemd-sysusers-mutable.nix @@ -63,6 +63,9 @@ in print(machine.succeed("getent passwd sysuser")) assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n" + with subtest("Manually add new user"): + machine.succeed("useradd manual-sysuser") + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")