Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport release-24.11] Correct password option docs and add related tests #367384

Merged
merged 4 commits into from
Dec 25, 2024
Merged
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
101 changes: 67 additions & 34 deletions nixos/modules/config/users-groups.nix
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,71 @@ let
"*" # password unset
]);

overrideOrderMutable = ''{option}`initialHashedPassword` -> {option}`initialPassword` -> {option}`hashedPassword` -> {option}`password` -> {option}`hashedPasswordFile`'';

overrideOrderImmutable = ''{option}`initialHashedPassword` -> {option}`hashedPassword` -> {option}`initialPassword` -> {option}`password` -> {option}`hashedPasswordFile`'';

overrideOrderText = isMutable: ''
If the option {option}`users.mutableUsers` is
`${if isMutable then "true" else "false"}`, then the order of precedence is as shown
below, where values on the left are overridden by values on the right:
${if isMutable then overrideOrderMutable else overrideOrderImmutable}
'';

multiplePasswordsWarning = ''
If multiple of these password options are set at the same time then a
specific order of precedence is followed, which can lead to surprising
results. The order of precedence differs depending on whether the
{option}`users.mutableUsers` option is set.
'';

overrideDescription = ''
${multiplePasswordsWarning}

${overrideOrderText false}

${overrideOrderText true}
'';

passwordDescription = ''
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`.
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
password defined in one of the three options will only be set when
the user is created for the first time. After that, you are free to
change the password with the ordinary user management commands. If
{option}`users.mutableUsers` is false, you cannot change
user passwords, they will always be set according to the password
options.
The {option}`initialHashedPassword`, {option}`hashedPassword`,
{option}`initialPassword`, {option}`password` and
{option}`hashedPasswordFile` options all control what password is set for
the user.

In a system where [](#opt-systemd.sysusers.enable) is `false`, typically
only one of {option}`hashedPassword`, {option}`password`, or
{option}`hashedPasswordFile` will be set.

In a system where [](#opt-systemd.sysusers.enable) is `true`, typically
only one of {option}`initialPassword`, {option}`initialHashedPassword`,
or {option}`hashedPasswordFile` will be set.

If the option {option}`users.mutableUsers` is true, the password defined
in one of the above password options will only be set when the user is
created for the first time. After that, you are free to change the
password with the ordinary user management commands. If
{option}`users.mutableUsers` is false, you cannot change user passwords,
they will always be set according to the password options.

If none of the password options are set, then no password is assigned to
the user, and the user will not be able to do password-based logins.

${overrideDescription}
'';

hashedPasswordDescription = ''
To generate a hashed password run `mkpasswd`.

If set to an empty string (`""`), this user will
be able to log in without being asked for a password (but not via remote
services such as SSH, or indirectly via {command}`su` or
{command}`sudo`). This should only be used for e.g. bootable
live systems. Note: this is different from setting an empty password,
which can be achieved using {option}`users.users.<name?>.password`.
If set to an empty string (`""`), this user will be able to log in without
being asked for a password (but not via remote services such as SSH, or
indirectly via {command}`su` or {command}`sudo`). This should only be used
for e.g. bootable live systems. Note: this is different from setting an
empty password, which can be achieved using
{option}`users.users.<name?>.password`.

If set to `null` (default) this user will not
be able to log in using a password (i.e. via {command}`login`
command).
If set to `null` (default) this user will not be able to log in using a
password (i.e. via {command}`login` command).
'';

userOpts = { name, config, ... }: {
Expand Down Expand Up @@ -281,6 +315,7 @@ let
default = null;
description = ''
Specifies the hashed password for the user.

${passwordDescription}
${hashedPasswordDescription}
'';
Expand All @@ -294,6 +329,7 @@ let
Warning: do not set confidential information here
because it is world-readable in the Nix store. This option
should only be used for public accounts.

${passwordDescription}
'';
};
Expand All @@ -307,6 +343,7 @@ let
password. The password file is read on each system activation. The
file should contain exactly one line, which should be the password in
an encrypted form that is suitable for the `chpasswd -e` command.

${passwordDescription}
'';
};
Expand All @@ -329,9 +366,7 @@ let
{command}`passwd` command. Otherwise, it's
equivalent to setting the {option}`hashedPassword` option.

Note that the {option}`hashedPassword` option will override
this option if both are set.

${passwordDescription}
${hashedPasswordDescription}
'';
};
Expand All @@ -351,8 +386,7 @@ let
used for guest accounts or passwords that will be changed
promptly.

Note that the {option}`password` option will override this
option if both are set.
${passwordDescription}
'';
};

Expand Down Expand Up @@ -960,12 +994,11 @@ 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.
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.
`initialHashedPassword`, `hashedPassword`, `initialPassword`, `password`
& `hashedPasswordFile` set to a non-null value.

${multiplePasswordsWarning}
${overrideOrderText cfg.mutableUsers}
The values of these options are:
${concatMapStringsSep
"\n"
Expand Down
2 changes: 2 additions & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,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 {};
Expand Down Expand Up @@ -1016,6 +1017,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 {};
Expand Down
171 changes: 171 additions & 0 deletions nixos/tests/password-option-override-ordering.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
let
password1 = "foobar";
password2 = "helloworld";
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 =
let
# The following users are expected to have the same behavior between immutable and mutable systems
# NOTE: Below given A -> B it implies B overrides A . Each entry below builds off the next
users = {
# mutable true/false: initialHashedPassword -> hashedPassword
fran = {
isNormalUser = true;
initialHashedPassword = hashed_yeshash;
hashedPassword = hashed_sha512crypt;
};

# mutable false: initialHashedPassword -> hashedPassword -> initialPassword
# mutable true: initialHashedPassword -> initialPassword -> hashedPassword
greg = {
isNormalUser = true;
hashedPassword = hashed_sha512crypt;
initialPassword = password1;
};

# mutable false: initialHashedPassword -> hashedPassword -> initialPassword -> password
# mutable true: initialHashedPassword -> initialPassword -> hashedPassword -> password
egon = {
isNormalUser = true;
initialPassword = password2;
password = password1;
};

# mutable true/false: hashedPassword -> password
# NOTE: minor duplication of test above, but to verify no initialXXX use is consistent
alice = {
isNormalUser = true;
hashedPassword = hashed_sha512crypt;
password = password1;
};

# mutable false: initialHashedPassword -> hashedPassword -> initialPassword -> password -> hashedPasswordFile
# mutable true: initialHashedPassword -> initialPassword -> hashedPassword -> password -> hashedPasswordFile
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 (which was
# the wording prior to PR 310484) is in fact 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.
# ```
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 also explicitly show the lib.trace explained above (see cat user) is wrong
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
};
};

mkTestMachine = mutable: {
environment.systemPackages = [ pkgs.shadow ];
users = {
mutableUsers = mutable;
inherit users;
};
};
in
{
immutable = mkTestMachine false;
mutable = mkTestMachine true;
};

testScript = ''
import crypt

def assert_password_match(machine, username, password):
shadow_entry = machine.succeed(f"getent shadow {username}")
print(shadow_entry)
hash = shadow_entry.split(":")[1]
seed = "$".join(hash.split("$")[:-1])
assert crypt.crypt(password, seed) == hash, f"{username} user password does not match"

with subtest("alice user has correct password"):
for machine in machines:
assert_password_match(machine, "alice", "${password1}")
assert "${hashed_sha512crypt}" not in machine.succeed("getent shadow alice"), f"{machine}: alice user password is not correct"

with subtest("bob user has correct password"):
for machine in machines:
print(machine.succeed("getent shadow bob"))
assert "${hashed_bcrypt}" in machine.succeed("getent shadow bob"), f"{machine}: bob user password is not correct"

with subtest("cat user has correct password"):
for machine in machines:
print(machine.succeed("getent shadow cat"))
assert "${hashed_bcrypt}" in machine.succeed("getent shadow cat"), f"{machine}: cat user password is not correct"

with subtest("dan user has correct password"):
for machine in machines:
print(machine.succeed("getent shadow dan"))
assert "${hashed_bcrypt}" in machine.succeed("getent shadow dan"), f"{machine}: dan user password is not correct"

with subtest("greg user has correct password"):
print(mutable.succeed("getent shadow greg"))
assert "${hashed_sha512crypt}" in mutable.succeed("getent shadow greg"), "greg user password is not correct"

assert_password_match(immutable, "greg", "${password1}")
assert "${hashed_sha512crypt}" not in immutable.succeed("getent shadow greg"), "greg user password is not correct"

for machine in machines:
machine.wait_for_unit("multi-user.target")
machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")

def check_login(machine: Machine, tty_number: str, username: str, password: str):
machine.send_key(f"alt-f{tty_number}")
machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
machine.wait_for_unit(f"getty@tty{tty_number}.service")
machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
machine.wait_until_tty_matches(tty_number, "login: ")
machine.send_chars(f"{username}\n")
machine.wait_until_tty_matches(tty_number, f"login: {username}")
machine.wait_until_succeeds("pgrep login")
machine.wait_until_tty_matches(tty_number, "Password: ")
machine.send_chars(f"{password}\n")
machine.send_chars(f"whoami > /tmp/{tty_number}\n")
machine.wait_for_file(f"/tmp/{tty_number}")
assert username in machine.succeed(f"cat /tmp/{tty_number}"), f"{machine}: {username} password is not correct"

with subtest("Test initialPassword override"):
for machine in machines:
check_login(machine, "2", "egon", "${password1}")

with subtest("Test initialHashedPassword override"):
for machine in machines:
check_login(machine, "3", "fran", "meow")
'';
}
)
Loading
Loading