From 38688bbc8ffb16a449a41cab344c27f6b1e74cd3 Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Tue, 30 Jul 2024 18:20:13 -0600 Subject: [PATCH 1/4] test: extend firstboot testing Several features were not being tested or weren't being evaluated thoroughly. --- test/units/TEST-74-AUX-UTILS.firstboot.sh | 62 +++++++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 7bab009d529..2569ad88167 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -14,6 +14,7 @@ fi at_exit() { if [[ -n "${ROOT:-}" ]]; then ls -lR "$ROOT" + grep -r . "$ROOT/etc" || : rm -fr "$ROOT" fi @@ -83,13 +84,27 @@ grep -q "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "$ROOT/etc/machine-id" rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" systemd-firstboot --root="$ROOT" --root-password=foo grep -q "^root:x:0:0:" "$ROOT/etc/passwd" -grep -q "^root:" "$ROOT/etc/shadow" +grep -q "^root:[^!*]" "$ROOT/etc/shadow" rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" echo "foo" >root.passwd systemd-firstboot --root="$ROOT" --root-password-file=root.passwd grep -q "^root:x:0:0:" "$ROOT/etc/passwd" -grep -q "^root:" "$ROOT/etc/shadow" +grep -q "^root:[^!*]" "$ROOT/etc/shadow" rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" root.passwd +# If /etc/passwd and /etc/shadow exist, they will only be updated if the shadow +# password is !unprovisioned. +echo "root:x:0:0:root:/root:/bin/sh" >"$ROOT/etc/passwd" +echo "root:!test:::::::" >"$ROOT/etc/shadow" +systemd-firstboot --root="$ROOT" --root-password=foo +grep -q "^root:x:0:0:" "$ROOT/etc/passwd" +grep -q "^root:!test:" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" +echo "root:x:0:0:root:/root:/bin/sh" >"$ROOT/etc/passwd" +echo "root:!unprovisioned:::::::" >"$ROOT/etc/shadow" +systemd-firstboot --root="$ROOT" --root-password=foo +grep -q "^root:x:0:0:" "$ROOT/etc/passwd" +grep -q "^root:[^!*]" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" # Set the shell together with the password, as firstboot won't touch # /etc/passwd if it already exists systemd-firstboot --root="$ROOT" --root-password-hashed="$ROOT_HASHED_PASSWORD1" --root-shell=/bin/fooshell @@ -176,8 +191,9 @@ mkdir -p "$ROOT/bin" touch "$ROOT/bin/fooshell" "$ROOT/bin/barshell" # Temporarily disable pipefail to avoid `echo: write error: Broken pipe set +o pipefail -# We can do only limited testing here, since it's all an interactive stuff, -# so --prompt and --prompt-root-password are skipped on purpose +# We can do only limited testing here, since it's all an interactive stuff, so +# --prompt is skipped on purpose and only limited --prompt-root-password +# testing can be done. echo -ne "\nfoo\nbar\n" | systemd-firstboot --root="$ROOT" --prompt-locale grep -q "LANG=foo" "$ROOT$LOCALE_PATH" grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" @@ -193,6 +209,11 @@ echo -ne "\nEurope/Berlin\n" | systemd-firstboot --root="$ROOT" --prompt-timezon readlink "$ROOT/etc/localtime" | grep -q "Europe/Berlin$" echo -ne "\nfoobar\n" | systemd-firstboot --root="$ROOT" --prompt-hostname grep -q "foobar" "$ROOT/etc/hostname" +# With no root password provided, a locked account should be created. +systemd-firstboot --root="$ROOT" --prompt-root-password Date: Tue, 30 Jul 2024 11:11:11 -0600 Subject: [PATCH 2/4] firstboot: create locked and empty root passwords consistently Although locked and empty passwords in /etc/passwd are treated the same, in all other cases the entry is configured to read the password from /etc/shadow. --- src/firstboot/firstboot.c | 11 +++++++---- test/units/TEST-74-AUX-UTILS.firstboot.sh | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index c70bfa468fd..acbe3e29b68 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -1142,10 +1142,13 @@ static int process_root_account(int rfd) { password = PASSWORD_SEE_SHADOW; hashed_password = _hashed_password; - } else if (arg_delete_root_password) - password = hashed_password = PASSWORD_NONE; - else - password = hashed_password = PASSWORD_LOCKED_AND_INVALID; + } else if (arg_delete_root_password) { + password = PASSWORD_SEE_SHADOW; + hashed_password = PASSWORD_NONE; + } else { + password = PASSWORD_SEE_SHADOW; + hashed_password = PASSWORD_LOCKED_AND_INVALID; + } r = write_root_passwd(rfd, pfd, password, arg_root_shell); if (r < 0) diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 2569ad88167..48792c4c676 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -211,7 +211,7 @@ echo -ne "\nfoobar\n" | systemd-firstboot --root="$ROOT" --prompt-hostname grep -q "foobar" "$ROOT/etc/hostname" # With no root password provided, a locked account should be created. systemd-firstboot --root="$ROOT" --prompt-root-password Date: Tue, 30 Jul 2024 13:42:26 -0600 Subject: [PATCH 3/4] firstboot: handle missing root password entries If /etc/passwd and/or /etc/shadow exist but don't have an existing root entry, one needs to be added. Previously this only worked if the files didn't exist. --- src/firstboot/firstboot.c | 24 +++++++++++++++-------- test/units/TEST-74-AUX-UTILS.firstboot.sh | 7 +++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index acbe3e29b68..b6512b05a4d 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -908,6 +908,7 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch _cleanup_fclose_ FILE *original = NULL, *passwd = NULL; _cleanup_(unlink_and_freep) char *passwd_tmp = NULL; int r; + bool found = false; assert(password); @@ -932,6 +933,7 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch i->pw_passwd = (char *) password; if (shell) i->pw_shell = (char *) shell; + found = true; } r = putpwent_sane(i, passwd); @@ -942,6 +944,12 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch return r; } else { + r = fchmod(fileno(passwd), 0644); + if (r < 0) + return -errno; + } + + if (!found) { struct passwd root = { .pw_name = (char *) "root", .pw_passwd = (char *) password, @@ -955,10 +963,6 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch if (errno != ENOENT) return -errno; - r = fchmod(fileno(passwd), 0644); - if (r < 0) - return -errno; - r = putpwent_sane(&root, passwd); if (r < 0) return r; @@ -979,6 +983,7 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { _cleanup_fclose_ FILE *original = NULL, *shadow = NULL; _cleanup_(unlink_and_freep) char *shadow_tmp = NULL; int r; + bool found = false; assert(hashed_password); @@ -1002,6 +1007,7 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { if (streq(i->sp_namp, "root")) { i->sp_pwdp = (char *) hashed_password; i->sp_lstchg = (long) (now(CLOCK_REALTIME) / USEC_PER_DAY); + found = true; } r = putspent_sane(i, shadow); @@ -1012,6 +1018,12 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { return r; } else { + r = fchmod(fileno(shadow), 0000); + if (r < 0) + return -errno; + } + + if (!found) { struct spwd root = { .sp_namp = (char*) "root", .sp_pwdp = (char *) hashed_password, @@ -1027,10 +1039,6 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { if (errno != ENOENT) return -errno; - r = fchmod(fileno(shadow), 0000); - if (r < 0) - return -errno; - r = putspent_sane(&root, shadow); if (r < 0) return r; diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 48792c4c676..7617b01bca7 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -91,6 +91,13 @@ systemd-firstboot --root="$ROOT" --root-password-file=root.passwd grep -q "^root:x:0:0:" "$ROOT/etc/passwd" grep -q "^root:[^!*]" "$ROOT/etc/shadow" rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" root.passwd +# Make sure the root password is set if /etc/passwd and /etc/shadow exist but +# don't have a root entry. +touch "$ROOT/etc/passwd" "$ROOT/etc/shadow" +systemd-firstboot --root="$ROOT" --root-password=foo +grep -q "^root:x:0:0:" "$ROOT/etc/passwd" +grep -q "^root:[^!*]" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" # If /etc/passwd and /etc/shadow exist, they will only be updated if the shadow # password is !unprovisioned. echo "root:x:0:0:root:/root:/bin/sh" >"$ROOT/etc/passwd" From 35bc4c34240afdd55e117b909f26fa9a5dc54f3b Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Tue, 30 Jul 2024 07:37:40 -0600 Subject: [PATCH 4/4] firstboot: fix root params with creds and prompting disabled Remove an early return that prevents --prompt-root-password or --prompt-root-shell and systemd.firstboot=off using credentials. In that case, arg_prompt_root_password and arg_prompt_root_shell will be false, but the prompt helpers still need to be called to read the credentials. Furthermore, if only the root shell has been set, don't overwrite the root password. --- src/firstboot/firstboot.c | 35 ++++++++++++----------- test/units/TEST-74-AUX-UTILS.firstboot.sh | 10 +++++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index b6512b05a4d..c452ca59756 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -910,8 +910,6 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch int r; bool found = false; - assert(password); - r = fopen_temporary_at_label(etc_fd, "passwd", "passwd", &passwd, &passwd_tmp); if (r < 0) return r; @@ -930,7 +928,8 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch while ((r = fgetpwent_sane(original, &i)) > 0) { if (streq(i->pw_name, "root")) { - i->pw_passwd = (char *) password; + if (password) + i->pw_passwd = (char *) password; if (shell) i->pw_shell = (char *) shell; found = true; @@ -952,7 +951,7 @@ static int write_root_passwd(int rfd, int etc_fd, const char *password, const ch if (!found) { struct passwd root = { .pw_name = (char *) "root", - .pw_passwd = (char *) password, + .pw_passwd = (char *) (password ?: PASSWORD_SEE_SHADOW), .pw_uid = 0, .pw_gid = 0, .pw_gecos = (char *) "Super User", @@ -985,8 +984,6 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { int r; bool found = false; - assert(hashed_password); - r = fopen_temporary_at_label(etc_fd, "shadow", "shadow", &shadow, &shadow_tmp); if (r < 0) return r; @@ -1005,8 +1002,10 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { while ((r = fgetspent_sane(original, &i)) > 0) { if (streq(i->sp_namp, "root")) { - i->sp_pwdp = (char *) hashed_password; - i->sp_lstchg = (long) (now(CLOCK_REALTIME) / USEC_PER_DAY); + if (hashed_password) { + i->sp_pwdp = (char *) hashed_password; + i->sp_lstchg = (long) (now(CLOCK_REALTIME) / USEC_PER_DAY); + } found = true; } @@ -1026,7 +1025,7 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { if (!found) { struct spwd root = { .sp_namp = (char*) "root", - .sp_pwdp = (char *) hashed_password, + .sp_pwdp = (char *) (hashed_password ?: PASSWORD_LOCKED_AND_INVALID), .sp_lstchg = (long) (now(CLOCK_REALTIME) / USEC_PER_DAY), .sp_min = -1, .sp_max = -1, @@ -1089,13 +1088,6 @@ static int process_root_account(int rfd) { return 0; } - /* Don't create/modify passwd and shadow if not asked */ - if (!(arg_root_password || arg_prompt_root_password || arg_copy_root_password || arg_delete_root_password || - arg_root_shell || arg_prompt_root_shell || arg_copy_root_shell)) { - log_debug("Initialization of root account was not requested, skipping."); - return 0; - } - r = make_lock_file_at(pfd, ETC_PASSWD_LOCK_FILENAME, LOCK_EX, &lock); if (r < 0) return log_error_errno(r, "Failed to take a lock on /etc/passwd: %m"); @@ -1153,9 +1145,18 @@ static int process_root_account(int rfd) { } else if (arg_delete_root_password) { password = PASSWORD_SEE_SHADOW; hashed_password = PASSWORD_NONE; - } else { + } else if (!arg_root_password && arg_prompt_root_password) { + /* If the user was prompted, but no password was supplied, lock the account. */ password = PASSWORD_SEE_SHADOW; hashed_password = PASSWORD_LOCKED_AND_INVALID; + } else + /* Leave the password as is. */ + password = hashed_password = NULL; + + /* Don't create/modify passwd and shadow if there's nothing to do. */ + if (!(password || hashed_password || arg_root_shell)) { + log_debug("Initialization of root account was not requested, skipping."); + return 0; } r = write_root_passwd(rfd, pfd, password, arg_root_shell); diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 7617b01bca7..d9e5f594268 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -112,8 +112,14 @@ systemd-firstboot --root="$ROOT" --root-password=foo grep -q "^root:x:0:0:" "$ROOT/etc/passwd" grep -q "^root:[^!*]" "$ROOT/etc/shadow" rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" -# Set the shell together with the password, as firstboot won't touch -# /etc/passwd if it already exists +systemd-firstboot --root="$ROOT" --root-password-hashed="$ROOT_HASHED_PASSWORD1" +grep -q "^root:x:0:0:" "$ROOT/etc/passwd" +grep -q "^root:$ROOT_HASHED_PASSWORD1:" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" +systemd-firstboot --root="$ROOT" --root-shell=/bin/fooshell +grep -q "^root:x:0:0:.*:/bin/fooshell$" "$ROOT/etc/passwd" +grep -q "^root:!\*:" "$ROOT/etc/shadow" +rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" systemd-firstboot --root="$ROOT" --root-password-hashed="$ROOT_HASHED_PASSWORD1" --root-shell=/bin/fooshell grep -q "^root:x:0:0:.*:/bin/fooshell$" "$ROOT/etc/passwd" grep -q "^root:$ROOT_HASHED_PASSWORD1:" "$ROOT/etc/shadow"