diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 845f9fdaf68ef4..32a8c8f6d80e9c 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -57,9 +57,9 @@ let The options {option}`hashedPassword`, {option}`password` and {option}`hashedPasswordFile` controls what password is set for the user. - {option}`hashedPassword` overrides both - {option}`password` and {option}`hashedPasswordFile`. - {option}`password` overrides {option}`hashedPasswordFile`. + {option}`hashedPasswordFile` overrides both + {option}`password` and {option}`hashedPassword`. + {option}`hashedPassword` overrides {option}`password`. If none of these three options are set, no password is assigned to the user, and the user will not be able to do password logins. If the option {option}`users.mutableUsers` is true, the @@ -69,6 +69,10 @@ let {option}`users.mutableUsers` is false, you cannot change user passwords, they will always be set according to the password options. + + NOTE: one exception is if [](#opt-systemd.sysusers.enable) is true. In this case + one of `initialPassword`, `initialHashedPassword`, or `hashedPasswordFile` + must be set. The order of overriding is the order of three listed above. ''; hashedPasswordDescription = '' @@ -330,7 +334,8 @@ let equivalent to setting the {option}`hashedPassword` option. Note that the {option}`hashedPassword` option will override - this option if both are set. + this option if both are set. One exception to this rule is if + [](#opt-systemd.sysusers.enable) is true. ${hashedPasswordDescription} ''; @@ -352,7 +357,8 @@ let promptly. Note that the {option}`password` option will override this - option if both are set. + option if both are set. One exception to this rule is if + [](#opt-systemd.sysusers.enable) is true. ''; }; @@ -960,12 +966,16 @@ in { (filter (x: x != null) (map (flip getAttr user) passwordOptions)); in optional (!unambiguousPasswordConfiguration) '' The user '${user.name}' has multiple of the options - `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword` - & `initialHashedPassword` set to a non-null value. + `initialHashedPassword`, `hashedPassword`, `initialPassword`, `password` + & `hashedPasswordFile` set to a non-null value. The options silently discard others by the order of precedence given above which can lead to surprising results. To resolve this warning, set at most one of the options above to a non-`null` value. + NOTE: one exception is if [](#opt-systemd.sysusers.enable) is true. In this case + one of `initialPassword`, `initialHashedPassword`, or `hashedPasswordFile` + must be set. The order of overriding is the order of three listed above. + The values of these options are: ${concatMapStringsSep "\n" diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 37e005f128a2eb..6029033f0eb3ed 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -770,6 +770,7 @@ in { pantheon = handleTest ./pantheon.nix {}; paperless = handleTest ./paperless.nix {}; parsedmarc = handleTest ./parsedmarc {}; + password-option-override-ordering = handleTest ./password-option-override-ordering.nix {}; pdns-recursor = handleTest ./pdns-recursor.nix {}; peerflix = handleTest ./peerflix.nix {}; peering-manager = handleTest ./web-apps/peering-manager.nix {}; @@ -1013,6 +1014,7 @@ in { systemd-sysupdate = runTest ./systemd-sysupdate.nix; systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix; systemd-sysusers-immutable = runTest ./systemd-sysusers-immutable.nix; + systemd-sysusers-password-option-override-ordering = runTest ./systemd-sysusers-password-option-override-ordering.nix; systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-timesyncd-nscd-dnssec = handleTest ./systemd-timesyncd-nscd-dnssec.nix {}; systemd-user-linger = handleTest ./systemd-user-linger.nix {}; diff --git a/nixos/tests/password-option-override-ordering.nix b/nixos/tests/password-option-override-ordering.nix new file mode 100644 index 00000000000000..899a22c6f104a7 --- /dev/null +++ b/nixos/tests/password-option-override-ordering.nix @@ -0,0 +1,151 @@ +let + password1 = "foobar"; + password2 = "helloworld"; + password3 = "bazqux"; + password4 = "asdf123"; + hashed_bcrypt = "$2b$05$8xIEflrk2RxQtcVXbGIxs.Vl0x7dF1/JSv3cyX6JJt0npzkTCWvxK"; # fnord + hashed_yeshash = "$y$j9T$d8Z4EAf8P1SvM/aDFbxMS0$VnTXMp/Hnc7QdCBEaLTq5ZFOAFo2/PM0/xEAFuOE88."; # fnord + hashed_sha512crypt = "$6$ymzs8WINZ5wGwQcV$VC2S0cQiX8NVukOLymysTPn4v1zJoJp3NGyhnqyv/dAf4NWZsBWYveQcj6gEJr4ZUjRBRjM0Pj1L8TCQ8hUUp0"; # meow +in +import ./make-test-python.nix ( + { pkgs, ... }: + { + name = "password-option-override-ordering"; + meta = with pkgs.lib.maintainers; { + maintainers = [ fidgetingbits ]; + }; + + nodes.machine = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.shadow ]; + + users = { + mutableUsers = false; + + # NOTE: Below given A -> B it implies B overrides A . Each entry below builds off the next + + # initialHashedPassword -> hashedPassword + users.fran = { + isNormalUser = true; + initialHashedPassword = hashed_yeshash; + hashedPassword = hashed_sha512crypt; + }; + + # initialHashedPassword -> hashedPassword -> initialPassword + users.greg = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + initialPassword = password1; # Expect override of above + }; + + # initialHashedPassword -> hashedPassword -> initialPassword -> password + users.egon = { + isNormalUser = true; + initialPassword = password2; + password = password1; + }; + + # initialHashedPassword -> hashedPassword -> initialPassword -> password + # NOTE: duplication, but to verify no initialXXX use is consistent + users.alice = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + password = password1; + }; + + # initialHashedPassword -> hashedPassword -> initialPassword -> password -> hashedPasswordFile + users.bob = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + password = password1; + hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; # Expect override of everything above + }; + + # Show hashedPassword -> password -> hashedPasswordFile -> initialPassword is false + # to explicitly show the following lib.trace warning in users-groups.nix is wrong: + # ``` + # The user 'root' has multiple of the options + # `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword` + # & `initialHashedPassword` set to a non-null value. + # The options silently discard others by the order of precedence + # given above which can lead to surprising results. To resolve this warning, + # set at most one of the options above to a non-`null` value. + # ``` + users.cat = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + password = password1; + hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; + initialPassword = password2; # lib.trace message implies this overrides everything above + }; + + # Show hashedPassword -> password -> hashedPasswordFile -> initialHashedPassword is false + # to explicitly show the lib.trace shown above is wrong + users.dan = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + initialPassword = password2; + password = password1; + hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; + initialHashedPassword = hashed_yeshash; # lib.trace message implies this overrides everything above + }; + }; + }; + + testScript = '' + with subtest("alice user has correct password"): + print(machine.succeed("getent passwd alice")) + assert "${hashed_sha512crypt}" not in machine.succeed("getent shadow alice"), "alice user password is not correct" + + with subtest("bob user has correct password"): + print(machine.succeed("getent passwd bob")) + assert "${hashed_bcrypt}" in machine.succeed("getent shadow bob"), "bob user password is not correct" + + with subtest("cat user has correct password"): + print(machine.succeed("getent passwd cat")) + assert "${hashed_bcrypt}" in machine.succeed("getent shadow cat"), "cat user password is not correct" + + with subtest("dan user has correct password"): + print(machine.succeed("getent passwd dan")) + assert "${hashed_bcrypt}" in machine.succeed("getent shadow dan"), "dan user password is not correct" + + with subtest("greg user has correct password"): + print(machine.succeed("getent passwd greg")) + assert "${hashed_sha512crypt}" not in machine.succeed("getent shadow greg"), "greg user password is not correct" + + machine.wait_for_unit("multi-user.target") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") + + with subtest("Test initialPassword override"): + machine.send_key("alt-f2") + machine.wait_until_succeeds("[ $(fgconsole) = 2 ]") + machine.wait_for_unit("getty@tty2.service") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'") + machine.wait_until_tty_matches("2", "login: ") + machine.send_chars("egon\n") + machine.wait_until_tty_matches("2", "login: egon") + machine.wait_until_succeeds("pgrep login") + machine.sleep(2) + machine.send_chars("${password1}\n") + machine.send_chars("whoami > /tmp/1\n") + machine.wait_for_file("/tmp/1") + assert "egon" in machine.succeed("cat /tmp/1") + + with subtest("Test initialHashedPassword override"): + machine.send_key("alt-f3") + machine.wait_until_succeeds("[ $(fgconsole) = 3 ]") + machine.wait_for_unit("getty@tty3.service") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty3'") + machine.wait_until_tty_matches("3", "login: ") + machine.send_chars("fran\n") + machine.wait_until_tty_matches("3", "login: fran") + machine.wait_until_succeeds("pgrep login") + machine.sleep(2) + machine.send_chars("meow\n") + machine.send_chars("whoami > /tmp/3\n") + machine.wait_for_file("/tmp/3") + assert "fran" in machine.succeed("cat /tmp/3") + ''; + } +) diff --git a/nixos/tests/systemd-sysusers-password-option-override-ordering.nix b/nixos/tests/systemd-sysusers-password-option-override-ordering.nix new file mode 100644 index 00000000000000..4c145b25627a95 --- /dev/null +++ b/nixos/tests/systemd-sysusers-password-option-override-ordering.nix @@ -0,0 +1,120 @@ +{ + lib, + pkgs ? import ../.., + ... +}: +let + password = "test"; + password1 = "test1"; + hashedPassword = "$y$j9T$wLgKY231.8j.ciV2MfEXe1$P0k5j3bCwHgnwW0Ive3w4knrgpiA4TzhCYLAnHvDZ51"; # test + hashedPassword1 = "$y$j9T$s8TyQJtNImvobhGM5Nlez0$3E8/O8EVGuA4sr1OQmrzi8GrRcy/AEhj454JjAn72A2"; # test + hashed_sha512crypt = "$6$ymzs8WINZ5wGwQcV$VC2S0cQiX8NVukOLymysTPn4v1zJoJp3NGyhnqyv/dAf4NWZsBWYveQcj6gEJr4ZUjRBRjM0Pj1L8TCQ8hUUp0"; # meow + + hashedPasswordFile = pkgs.writeText "hashed-password" hashedPassword1; +in +{ + name = "systemd-sysusers-password-option-override-ordering"; + + meta.maintainers = with lib.maintainers; [ fidgetingbits ]; + + nodes.machine = { + systemd.sysusers.enable = true; + system.etc.overlay.enable = true; + boot.initrd.systemd.enable = true; + + users.mutableUsers = true; + + # NOTE: Below given A -> B it implies B overrides A . Each entry below builds off the next + + users.users.root = { + hashedPasswordFile = lib.mkForce null; + initialHashedPassword = password; + }; + + # initialPassword -> initialHashedPassword + users.users.alice = { + isNormalUser = true; + initialPassword = password; + initialHashedPassword = hashedPassword; + }; + + # initialPassword -> initialHashedPassword -> hashedPasswordFile + users.users.bob = { + isNormalUser = true; + initialPassword = password; + initialHashedPassword = hashedPassword; + hashedPasswordFile = hashedPasswordFile.outPath; + }; + + # Show that initialPassword -> password is not true for systemd-sysusers + users.users.cat = { + isNormalUser = true; + initialPassword = password; + password = password1; # We expect this not to override + }; + # Show that initialPassword -> password is not true for systemd-sysusers + users.users.dan = { + isNormalUser = true; + initialHashedPassword = hashedPassword; + hashedPassword = hashed_sha512crypt; # We expect this not to override + }; + }; + + testScript = '' + machine.wait_for_unit("systemd-sysusers.service") + + with subtest("systemd-sysusers.service contains the credentials"): + sysusers_service = machine.succeed("systemctl cat systemd-sysusers.service") + print(sysusers_service) + assert "SetCredential=passwd.plaintext-password.alice:${password}" in sysusers_service + + 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("alice user has correct password"): + print(machine.succeed("getent passwd alice")) + assert "${hashedPassword}" in machine.succeed("getent shadow alice"), "alice user password is not correct" + + with subtest("bob user has new password after switching to new generation"): + print(machine.succeed("getent passwd bob")) + print(machine.succeed("getent shadow bob")) + assert "${hashedPassword1}" in machine.succeed("getent shadow bob"), "bob user password is not correct" + + machine.wait_for_unit("multi-user.target") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") + + with subtest("Test initialPassword override"): + machine.send_key("alt-f2") + machine.wait_until_succeeds("[ $(fgconsole) = 2 ]") + machine.wait_for_unit("getty@tty2.service") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'") + machine.wait_until_tty_matches("2", "login: ") + machine.send_chars("cat\n") + machine.wait_until_tty_matches("2", "login: cat") + machine.wait_until_succeeds("pgrep login") + machine.sleep(2) + machine.send_chars("${password}\n") + machine.send_chars("whoami > /tmp/1\n") + machine.wait_for_file("/tmp/1") + assert "cat" in machine.succeed("cat /tmp/1") + + with subtest("Test initialHashedPassword override"): + machine.send_key("alt-f3") + machine.wait_until_succeeds("[ $(fgconsole) = 3 ]") + machine.wait_for_unit("getty@tty3.service") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty3'") + machine.wait_until_tty_matches("3", "login: ") + machine.send_chars("dan\n") + machine.wait_until_tty_matches("3", "login: dan") + machine.wait_until_succeeds("pgrep login") + machine.sleep(2) + machine.send_chars("test\n") + machine.send_chars("whoami > /tmp/3\n") + machine.wait_for_file("/tmp/3") + assert "dan" in machine.succeed("cat /tmp/3") + + ''; +}