From 6f496236bc3241a9aa191d9e084f566771286d3a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Sep 2025 16:09:02 +0200 Subject: [PATCH 1/8] man: document --prompt-new-user which was added back in v256 --- man/homectl.xml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index 36b3bbfe9c..83e2286acb 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -205,6 +205,16 @@ + + + + If used in conjunction with firstboot and no regular user account + exists on the system so far the tool will interactively query for user information and create an + account. + + + + @@ -1424,9 +1434,10 @@ $ ssh lennart@targetsystem This command is supposed to be invoked during the initial boot of the system. It checks whether any regular home area exists so far, and if not queries the user interactively on the - console for user name and password and creates one. Alternatively, if one or more service credentials - whose name starts with home.create. are passed to the command (containing a user - record in JSON format) these users are automatically created at boot. + console for user name and password and creates one (only if is + specified). Alternatively, if one or more service credentials whose name starts with + home.create. are passed to the command (containing a user record in JSON format) + these users are automatically created at boot. This command is invoked by the systemd-homed-firstboot.service service unit. From 6d40d0773c5c7a5ba7834688708174cb6947ad48 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Sep 2025 08:54:36 +0200 Subject: [PATCH 2/8] homectl: make querying for shell+aux groups optional in firstboot mode --- man/homectl.xml | 19 +++ src/home/homectl.c | 207 +++++++++++++++++--------- units/systemd-homed-firstboot.service | 2 +- 3 files changed, 154 insertions(+), 74 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index 83e2286acb..d57001ab03 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -215,6 +215,25 @@ + + + + Takes a boolean argument. Controls whether to interactively query for a login shell, + if firstboot --prompt-new-user is used. Defaults to true. + + + + + + + + Takes a boolean argument. Controls whether to interactively query for auxiliary + groups to add the new user to, if firstboot --prompt-new-user is used. Defaults to + true. + + + + diff --git a/src/home/homectl.c b/src/home/homectl.c index c50302fb74..69771972d0 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -101,13 +101,15 @@ static enum { } arg_export_format = EXPORT_FORMAT_FULL; static uint64_t arg_capability_bounding_set = UINT64_MAX; static uint64_t arg_capability_ambient_set = UINT64_MAX; -static bool arg_prompt_new_user = false; static char *arg_blob_dir = NULL; static bool arg_blob_clear = false; static Hashmap *arg_blob_files = NULL; static char *arg_key_name = NULL; static bool arg_dry_run = false; static bool arg_seize = true; +static bool arg_prompt_new_user = false; +static bool arg_prompt_shell = true; +static bool arg_prompt_groups = true; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); @@ -2680,77 +2682,19 @@ static int group_completion_callback(const char *key, char ***ret_list, void *us return 0; } -static int create_interactively(void) { - _cleanup_free_ char *username = NULL; +static int prompt_groups(const char *username, char ***ret_groups) { int r; - if (!arg_prompt_new_user) { - log_debug("Prompting for user creation was not requested."); + assert(username); + assert(ret_groups); + + if (!arg_prompt_groups) { + *ret_groups = NULL; return 0; } - putchar('\n'); - if (emoji_enabled()) { - fputs(glyph(GLYPH_HOME), stdout); - putchar(' '); - } - printf("Please create your user account!\n"); - - if (!any_key_to_proceed()) { - log_notice("Skipping."); - return 0; - } - - (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); - - for (;;) { - username = mfree(username); - - r = ask_string(&username, - "%s Please enter user name to create (empty to skip): ", - glyph(GLYPH_TRIANGULAR_BULLET)); - if (r < 0) - return log_error_errno(r, "Failed to query user for username: %m"); - - if (isempty(username)) { - log_info("No data entered, skipping."); - return 0; - } - - if (!valid_user_group_name(username, /* flags= */ 0)) { - log_notice("Specified user name is not a valid UNIX user name, try again: %s", username); - continue; - } - - r = userdb_by_name(username, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); - if (r == -ESRCH) - break; - if (r < 0) - return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username); - - log_notice("Specified user '%s' exists already, try again.", username); - } - - r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username); - if (r < 0) - return log_error_errno(r, "Failed to set userName field: %m"); - - /* Let's not insist on a strong password in the firstboot interactive interface. Insisting on this is - * really annoying, as the user cannot just invoke the tool again with "--enforce-password-policy=no" - * because after all the tool is called from the boot process, and not from an interactive - * shell. Moreover, when setting up an initial system we can assume the user owns it, and hence we - * don't need to hard enforce some policy on password strength some organization or OS vendor - * requires. Note that this just disables the *strict* enforcement of the password policy. Even with - * this disabled we'll still tell the user in the UI that the password is too weak and suggest better - * ones, even if we then accept the weak ones if the user insists, by repeating it. */ - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); - if (r < 0) - return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m"); - _cleanup_strv_free_ char **available = NULL, **groups = NULL; for (;;) { - _cleanup_free_ char *s = NULL; - strv_sort_uniq(groups); if (!strv_isempty(groups)) { @@ -2761,6 +2705,7 @@ static int create_interactively(void) { log_info("Currently selected groups: %s", j); } + _cleanup_free_ char *s = NULL; r = ask_string_full(&s, group_completion_callback, &available, "%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ", @@ -2834,16 +2779,22 @@ static int create_interactively(void) { return log_oom(); } - if (!strv_isempty(groups)) { - strv_sort_uniq(groups); + *ret_groups = TAKE_PTR(groups); + return 0; +} - r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups); - if (r < 0) - return log_error_errno(r, "Failed to set memberOf field: %m"); +static int prompt_shell(const char *username, char **ret_shell) { + int r; + + assert(username); + assert(ret_shell); + + if (!arg_prompt_shell) { + *ret_shell = NULL; + return 0; } _cleanup_free_ char *shell = NULL; - for (;;) { shell = mfree(shell); @@ -2873,6 +2824,95 @@ static int create_interactively(void) { log_notice("Specified shell '%s' is not installed, try another one.", shell); } + *ret_shell = TAKE_PTR(shell); + return 0; +} + +static int create_interactively(void) { + _cleanup_free_ char *username = NULL; + int r; + + if (!arg_prompt_new_user) { + log_debug("Prompting for user creation was not requested."); + return 0; + } + + putchar('\n'); + if (emoji_enabled()) { + fputs(glyph(GLYPH_HOME), stdout); + putchar(' '); + } + printf("Please create your user account!\n"); + + if (!any_key_to_proceed()) { + log_notice("Skipping."); + return 0; + } + + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); + + for (;;) { + username = mfree(username); + + r = ask_string(&username, + "%s Please enter user name to create (empty to skip): ", + glyph(GLYPH_TRIANGULAR_BULLET)); + if (r < 0) + return log_error_errno(r, "Failed to query user for username: %m"); + + if (isempty(username)) { + log_info("No data entered, skipping."); + return 0; + } + + if (!valid_user_group_name(username, /* flags= */ 0)) { + log_notice("Specified user name is not a valid UNIX user name, try again: %s", username); + continue; + } + + r = userdb_by_name(username, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); + if (r == -ESRCH) + break; + if (r < 0) + return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username); + + log_notice("Specified user '%s' exists already, try again.", username); + } + + r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username); + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + + /* Let's not insist on a strong password in the firstboot interactive interface. Insisting on this is + * really annoying, as the user cannot just invoke the tool again with "--enforce-password-policy=no" + * because after all the tool is called from the boot process, and not from an interactive + * shell. Moreover, when setting up an initial system we can assume the user owns it, and hence we + * don't need to hard enforce some policy on password strength some organization or OS vendor + * requires. Note that this just disables the *strict* enforcement of the password policy. Even with + * this disabled we'll still tell the user in the UI that the password is too weak and suggest better + * ones, even if we then accept the weak ones if the user insists, by repeating it. */ + r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); + if (r < 0) + return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m"); + + _cleanup_strv_free_ char **groups = NULL; + r = prompt_groups(username, &groups); + if (r < 0) + return r; + + if (!strv_isempty(groups)) { + strv_sort_uniq(groups); + + r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups); + if (r < 0) + return log_error_errno(r, "Failed to set memberOf field: %m"); + } + + _cleanup_free_ char *shell = NULL; + r = prompt_shell(username, &shell); + if (r < 0) + return r; + if (!isempty(shell)) { log_info("Selected %s as the shell for user %s", shell, username); @@ -3021,11 +3061,14 @@ static int help(int argc, char *argv[], void *userdata) { " -E When specified once equals -j --export-format=\n" " stripped, when specified twice equals\n" " -j --export-format=minimal\n" - " --prompt-new-user firstboot: Query user interactively for user\n" - " to create\n" " --key-name=NAME Key name when adding a signing key\n" " --seize=no Do not strip existing signatures of user record\n" " when creating\n" + " --prompt-new-user firstboot: Query user interactively for user\n" + " to create\n" + " --prompt-groups=no In first-boot mode, don't prompt for auxiliary\n" + " group memberships\n" + " --prompt-shell=no In first-boot mode, don't prompt for shells\n" "\n%4$sGeneral User Record Properties:%5$s\n" " -c --real-name=REALNAME Real name for user\n" " --realm=REALM Realm to create user in\n" @@ -3262,6 +3305,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_KEY_NAME, ARG_SEIZE, ARG_MATCH, + ARG_PROMPT_SHELL, + ARG_PROMPT_GROUPS, }; static const struct option options[] = { @@ -3368,6 +3413,8 @@ static int parse_argv(int argc, char *argv[]) { { "key-name", required_argument, NULL, ARG_KEY_NAME }, { "seize", required_argument, NULL, ARG_SEIZE }, { "match", required_argument, NULL, ARG_MATCH }, + { "prompt-shell", required_argument, NULL, ARG_PROMPT_SHELL }, + { "prompt-groups", required_argument, NULL, ARG_PROMPT_GROUPS }, {} }; @@ -4927,6 +4974,20 @@ static int parse_argv(int argc, char *argv[]) { match_identity = &arg_identity_extra_other_machines; break; + case ARG_PROMPT_SHELL: + r = parse_boolean_argument("--prompt-shell=", optarg, &arg_prompt_shell); + if (r < 0) + return r; + + break; + + case ARG_PROMPT_GROUPS: + r = parse_boolean_argument("--prompt-groups=", optarg, &arg_prompt_groups); + if (r < 0) + return r; + + break; + case '?': return -EINVAL; diff --git a/units/systemd-homed-firstboot.service b/units/systemd-homed-firstboot.service index 444ec3055d..bc92592415 100644 --- a/units/systemd-homed-firstboot.service +++ b/units/systemd-homed-firstboot.service @@ -17,7 +17,7 @@ Before=systemd-user-sessions.service first-boot-complete.target [Service] Type=oneshot RemainAfterExit=yes -ExecStart=homectl firstboot --prompt-new-user +ExecStart=homectl firstboot --prompt-new-user --prompt-shell=no --prompt-groups=no StandardOutput=tty StandardInput=tty StandardError=tty From b128c31f46e8990d16588e80dcd82ce8787c5024 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Sep 2025 10:18:29 +0200 Subject: [PATCH 3/8] homectl: show blue 'chrome' bar in first boot mode --- man/homectl.xml | 11 +++++++++++ src/home/homectl.c | 23 ++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index d57001ab03..961266aea3 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -234,6 +234,17 @@ + + + + Takes a boolean argument. By default the interactive user creation via + firstboot --prompt-new-user screen will show reverse color "chrome" bars at the + top and and the bottom of the terminal screen, which may be disabled by setting this option to + false. + + + + diff --git a/src/home/homectl.c b/src/home/homectl.c index 69771972d0..22665eae45 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -49,6 +49,7 @@ #include "pretty-print.h" #include "proc-cmdline.h" #include "process-util.h" +#include "prompt-util.h" #include "recurse-dir.h" #include "rlimit-util.h" #include "runtime-scope.h" @@ -110,6 +111,7 @@ static bool arg_seize = true; static bool arg_prompt_new_user = false; static bool arg_prompt_shell = true; static bool arg_prompt_groups = true; +static bool arg_chrome = true; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); @@ -2837,7 +2839,13 @@ static int create_interactively(void) { return 0; } - putchar('\n'); + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); + + if (arg_chrome) + chrome_show("Create a User Account", /* bottom= */ NULL); + + DEFER_VOID_CALL(chrome_hide); + if (emoji_enabled()) { fputs(glyph(GLYPH_HOME), stdout); putchar(' '); @@ -2849,8 +2857,6 @@ static int create_interactively(void) { return 0; } - (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); - for (;;) { username = mfree(username); @@ -3069,6 +3075,8 @@ static int help(int argc, char *argv[], void *userdata) { " --prompt-groups=no In first-boot mode, don't prompt for auxiliary\n" " group memberships\n" " --prompt-shell=no In first-boot mode, don't prompt for shells\n" + " --chrome=no In first-boot mode, don't show colour bar at top\n" + " and bottom of terminal\n" "\n%4$sGeneral User Record Properties:%5$s\n" " -c --real-name=REALNAME Real name for user\n" " --realm=REALM Realm to create user in\n" @@ -3307,6 +3315,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_MATCH, ARG_PROMPT_SHELL, ARG_PROMPT_GROUPS, + ARG_CHROME, }; static const struct option options[] = { @@ -3415,6 +3424,7 @@ static int parse_argv(int argc, char *argv[]) { { "match", required_argument, NULL, ARG_MATCH }, { "prompt-shell", required_argument, NULL, ARG_PROMPT_SHELL }, { "prompt-groups", required_argument, NULL, ARG_PROMPT_GROUPS }, + { "chrome", required_argument, NULL, ARG_CHROME }, {} }; @@ -4988,6 +4998,13 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_CHROME: + r = parse_boolean_argument("--chrome=", optarg, &arg_chrome); + if (r < 0) + return r; + + break; + case '?': return -EINVAL; From 0cfaea3619a97062d864a3eb977dab379f795815 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Sep 2025 10:22:49 +0200 Subject: [PATCH 4/8] homectl: mute console while running firstboot prompts --- man/homectl.xml | 10 ++++++++++ src/home/homectl.c | 16 ++++++++++++++++ units/systemd-homed-firstboot.service | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/man/homectl.xml b/man/homectl.xml index 961266aea3..82ea2e3943 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -245,6 +245,16 @@ + + + + Takes a boolean argument. If true kernel log output and service manager status output + to the system console is temporarily disabled while firstboot --prompt-new-user is + running, so that its own output is not interrupted. Defaults to false. + + + + diff --git a/src/home/homectl.c b/src/home/homectl.c index 22665eae45..199cb39537 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -4,6 +4,7 @@ #include #include "sd-bus.h" +#include "sd-varlink.h" #include "ask-password-api.h" #include "bitfield.h" @@ -112,6 +113,7 @@ static bool arg_prompt_new_user = false; static bool arg_prompt_shell = true; static bool arg_prompt_groups = true; static bool arg_chrome = true; +static bool arg_mute_console = false; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); @@ -2839,6 +2841,9 @@ static int create_interactively(void) { return 0; } + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL; + (void) mute_console(&mute_console_link); + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); if (arg_chrome) @@ -3077,6 +3082,8 @@ static int help(int argc, char *argv[], void *userdata) { " --prompt-shell=no In first-boot mode, don't prompt for shells\n" " --chrome=no In first-boot mode, don't show colour bar at top\n" " and bottom of terminal\n" + " --mute-console=yes In first-boot mode, tell kernel/PID 1 to not\n" + " write to the console while running\n" "\n%4$sGeneral User Record Properties:%5$s\n" " -c --real-name=REALNAME Real name for user\n" " --realm=REALM Realm to create user in\n" @@ -3316,6 +3323,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_PROMPT_SHELL, ARG_PROMPT_GROUPS, ARG_CHROME, + ARG_MUTE_CONSOLE, }; static const struct option options[] = { @@ -3425,6 +3433,7 @@ static int parse_argv(int argc, char *argv[]) { { "prompt-shell", required_argument, NULL, ARG_PROMPT_SHELL }, { "prompt-groups", required_argument, NULL, ARG_PROMPT_GROUPS }, { "chrome", required_argument, NULL, ARG_CHROME }, + { "mute-console", required_argument, NULL, ARG_MUTE_CONSOLE }, {} }; @@ -5005,6 +5014,13 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_MUTE_CONSOLE: + r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console); + if (r < 0) + return r; + + break; + case '?': return -EINVAL; diff --git a/units/systemd-homed-firstboot.service b/units/systemd-homed-firstboot.service index bc92592415..531c50f02d 100644 --- a/units/systemd-homed-firstboot.service +++ b/units/systemd-homed-firstboot.service @@ -17,7 +17,7 @@ Before=systemd-user-sessions.service first-boot-complete.target [Service] Type=oneshot RemainAfterExit=yes -ExecStart=homectl firstboot --prompt-new-user --prompt-shell=no --prompt-groups=no +ExecStart=homectl firstboot --prompt-new-user --prompt-shell=no --prompt-groups=no --mute-console=yes StandardOutput=tty StandardInput=tty StandardError=tty From f233132a67a4c2c6dff053afac2385f570e8e3fe Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Sep 2025 15:02:51 +0200 Subject: [PATCH 5/8] homectl: port to prompt_loop() --- src/home/homectl.c | 139 +++++++++++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 55 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 199cb39537..8c99120ea6 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -16,6 +16,7 @@ #include "capability-list.h" #include "capability-util.h" #include "cgroup-util.h" +#include "chase.h" #include "creds-util.h" #include "dirent-util.h" #include "dns-domain.h" @@ -2697,6 +2698,8 @@ static int prompt_groups(const char *username, char ***ret_groups) { return 0; } + putchar('\n'); + _cleanup_strv_free_ char **available = NULL, **groups = NULL; for (;;) { strv_sort_uniq(groups); @@ -2713,7 +2716,7 @@ static int prompt_groups(const char *username, char ***ret_groups) { r = ask_string_full(&s, group_completion_callback, &available, "%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ", - glyph(GLYPH_TRIANGULAR_BULLET), username); + glyph(GLYPH_LABEL), username); if (r < 0) return log_error_errno(r, "Failed to query user for auxiliary group: %m"); @@ -2787,9 +2790,32 @@ static int prompt_groups(const char *username, char ***ret_groups) { return 0; } -static int prompt_shell(const char *username, char **ret_shell) { +static int shell_is_ok(const char *path, void *userdata) { int r; + assert(path); + + if (!valid_shell(path)) { + log_error("String '%s' is not a valid path to a shell, refusing.", path); + return false; + } + + r = chase_and_access(path, /* root= */ NULL, CHASE_MUST_BE_REGULAR, X_OK, /* ret_path= */ NULL) >= 0; + if (r == -ENOENT) { + log_error_errno(r, "Shell '%s' does not exist, try again.", path); + return false; + } + if (ERRNO_IS_NEG_PRIVILEGE(r)) { + log_error_errno(r, "File '%s' is not executable, try again.", path); + return false; + } + if (r < 0) + return log_error_errno(r, "Failed to check if shell '%s' exists and is executable: %m", path); + + return true; +} + +static int prompt_shell(const char *username, char **ret_shell) { assert(username); assert(ret_shell); @@ -2798,38 +2824,45 @@ static int prompt_shell(const char *username, char **ret_shell) { return 0; } - _cleanup_free_ char *shell = NULL; - for (;;) { - shell = mfree(shell); + putchar('\n'); - r = ask_string(&shell, - "%s Please enter the shell to use for user %s (empty for default): ", - glyph(GLYPH_TRIANGULAR_BULLET), username); - if (r < 0) - return log_error_errno(r, "Failed to query user for username: %m"); + _cleanup_free_ char *q = strjoin("Please enter the shell to use for user ", username); + if (!q) + return log_oom(); - if (isempty(shell)) { - log_info("No data entered, leaving at default."); - break; - } + return prompt_loop( + q, + GLYPH_SHELL, + /* menu= */ NULL, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 0, + /* n_columns= */ 3, + /* column_width= */ 20, + shell_is_ok, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE, + ret_shell); +} - if (!valid_shell(shell)) { - log_notice("Specified shell is not a valid UNIX shell path, try again: %s", shell); - continue; - } +static int username_is_ok(const char *name, void *userdata) { + int r; - r = RET_NERRNO(access(shell, X_OK)); - if (r >= 0) - break; + assert(name); - if (r != -ENOENT) - return log_error_errno(r, "Failed to check if shell %s exists: %m", shell); - - log_notice("Specified shell '%s' is not installed, try another one.", shell); + if (!valid_user_group_name(name, /* flags= */ 0)) { + log_notice("Specified user name is not a valid UNIX user name, try again: %s", name); + return false; } - *ret_shell = TAKE_PTR(shell); - return 0; + r = userdb_by_name(name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); + if (r == -ESRCH) + return true; + if (r < 0) + return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", name); + + log_notice("Specified user '%s' exists already, try again.", name); + return false; } static int create_interactively(void) { @@ -2862,33 +2895,22 @@ static int create_interactively(void) { return 0; } - for (;;) { - username = mfree(username); - - r = ask_string(&username, - "%s Please enter user name to create (empty to skip): ", - glyph(GLYPH_TRIANGULAR_BULLET)); - if (r < 0) - return log_error_errno(r, "Failed to query user for username: %m"); - - if (isempty(username)) { - log_info("No data entered, skipping."); - return 0; - } - - if (!valid_user_group_name(username, /* flags= */ 0)) { - log_notice("Specified user name is not a valid UNIX user name, try again: %s", username); - continue; - } - - r = userdb_by_name(username, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); - if (r == -ESRCH) - break; - if (r < 0) - return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username); - - log_notice("Specified user '%s' exists already, try again.", username); - } + r = prompt_loop("Please enter user name to create", + GLYPH_IDCARD, + /* menu= */ NULL, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 60, + /* n_columns= */ 3, + /* column_width= */ 20, + username_is_ok, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE, + &username); + if (r < 0) + return r; + if (isempty(username)) + return 0; r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username); if (r < 0) @@ -2932,7 +2954,14 @@ static int create_interactively(void) { return log_error_errno(r, "Failed to set shell field: %m"); } - return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false); + putchar('\n'); + + r = create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false); + if (r < 0) + return r; + + log_info("Successfully created account '%s'.", username); + return 0; } static int add_signing_keys_from_credentials(void); From bedcce1a1f13a84126b9d8e8d9120ef8db515d4b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Sep 2025 10:17:00 +0200 Subject: [PATCH 6/8] firstboot: drop waiting for key when starting We only show this blurb on the first question, but the first question is interactive anyway, hence no need to wait for input first explicitly, we won't progress anyway without user input. (similar for homectl's firstboot) --- src/firstboot/firstboot.c | 4 +--- src/home/homectl.c | 7 +------ test/units/TEST-74-AUX-UTILS.firstboot.sh | 12 ++++++------ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 6a2e8ee284..9637d9f8f6 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -147,9 +147,7 @@ static void print_welcome(int rfd, sd_varlink **mute_console_link) { fputs(glyph(GLYPH_SPARKLES), stdout); putchar(' '); } - printf("Please configure your new system!\n"); - - any_key_to_proceed(); + printf("Please configure your new system!\n\n"); done = true; } diff --git a/src/home/homectl.c b/src/home/homectl.c index 8c99120ea6..6c83765766 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2888,12 +2888,7 @@ static int create_interactively(void) { fputs(glyph(GLYPH_HOME), stdout); putchar(' '); } - printf("Please create your user account!\n"); - - if (!any_key_to_proceed()) { - log_notice("Skipping."); - return 0; - } + printf("Please create your user account!\n\n"); r = prompt_loop("Please enter user name to create", GLYPH_IDCARD, diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 3c1ee5ed16..5f7209231a 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -211,7 +211,7 @@ set +o pipefail # 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 +echo -ne "foo\nbar\n" | systemd-firstboot --root="$ROOT" --prompt-locale grep -q "LANG=foo" "$ROOT$LOCALE_PATH" grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" # systemd-firstboot in prompt-keymap mode requires keymaps to be installed so @@ -219,22 +219,22 @@ grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" # compatible keymaps (from the kbd package), skip this test if the keymaps are # missing. if [ -d "/usr/share/keymaps/" ] || [ -d "/usr/share/kbd/keymaps/" ] || [ -d "/usr/lib/kbd/keymaps/" ] ; then - echo -ne "\nfoo\n" | systemd-firstboot --root="$ROOT" --prompt-keymap + echo -ne "foo\n" | systemd-firstboot --root="$ROOT" --prompt-keymap grep -q "KEYMAP=foo" "$ROOT/etc/vconsole.conf" fi -echo -ne "\nEurope/Berlin\n" | systemd-firstboot --root="$ROOT" --prompt-timezone +echo -ne "Europe/Berlin\n" | systemd-firstboot --root="$ROOT" --prompt-timezone readlink "$ROOT/etc/localtime" | grep -q "Europe/Berlin$" -echo -ne "\nfoobar\n" | systemd-firstboot --root="$ROOT" --prompt-hostname +echo -ne "foobar\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: Fri, 19 Sep 2025 15:42:32 +0200 Subject: [PATCH 7/8] firstboot: optionally, don't query for keymap unless connected to a real VT The keymap only really matters if there's local access to a system, i.e. if there's actually a physical kbd directly connected to it, to apply it to. If during firstboot we are not talked to via a VT (but via SSH, container, or hypervisor console or so instead), then it's very unlikely we ever are. Hence, don't ask for a keymap, and let#s shortcut the questions asked at boot. --- man/systemd-firstboot.xml | 11 +++++++++ src/firstboot/firstboot.c | 29 +++++++++++++++++++++-- test/units/TEST-74-AUX-UTILS.firstboot.sh | 4 ++++ units/systemd-firstboot.service | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/man/systemd-firstboot.xml b/man/systemd-firstboot.xml index cc4ddc0cf6..26ce757174 100644 --- a/man/systemd-firstboot.xml +++ b/man/systemd-firstboot.xml @@ -271,6 +271,17 @@ + + + + If invoked from a virtual terminal TTY equivalent to + , otherwise has no effect. Or in other words, only prompts + interactively for a keymap if a local keyboard is used for interactivity that requires it. + + + + + diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 9637d9f8f6..9f237dc266 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -71,6 +71,7 @@ static char *arg_root_shell = NULL; static char *arg_kernel_cmdline = NULL; static bool arg_prompt_locale = false; static bool arg_prompt_keymap = false; +static bool arg_prompt_keymap_auto = false; static bool arg_prompt_timezone = false; static bool arg_prompt_hostname = false; static bool arg_prompt_root_password = false; @@ -415,7 +416,21 @@ static int prompt_keymap(int rfd, sd_varlink **mute_console_link) { return 0; } - if (!arg_prompt_keymap) { + bool b; + if (arg_prompt_keymap_auto) { + _cleanup_free_ char *ttyname = NULL; + + r = getttyname_harder(STDOUT_FILENO, &ttyname); + if (r < 0) { + log_debug_errno(r, "Cannot determine TTY we are connected, ignoring: %m"); + b = false; /* if we can't resolve this, it's probably not a VT */ + } else { + b = tty_is_vc_resolve(ttyname); + log_debug("Detected connection to local console: %s", yes_no(b)); + } + } else + b = arg_prompt_keymap; + if (!b) { log_debug("Prompting for keymap was not requested."); return 0; } @@ -1234,6 +1249,8 @@ static int help(void) { " Set kernel command line\n" " --prompt-locale Prompt the user for locale settings\n" " --prompt-keymap Prompt the user for keymap settings\n" + " --prompt-keymap-auto Prompt the user for keymap settings if invoked\n" + " on local console\n" " --prompt-timezone Prompt the user for timezone\n" " --prompt-hostname Prompt the user for hostname\n" " --prompt-root-password Prompt the user for root password\n" @@ -1284,6 +1301,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_PROMPT, ARG_PROMPT_LOCALE, ARG_PROMPT_KEYMAP, + ARG_PROMPT_KEYMAP_AUTO, ARG_PROMPT_TIMEZONE, ARG_PROMPT_HOSTNAME, ARG_PROMPT_ROOT_PASSWORD, @@ -1323,6 +1341,7 @@ static int parse_argv(int argc, char *argv[]) { { "prompt", no_argument, NULL, ARG_PROMPT }, { "prompt-locale", no_argument, NULL, ARG_PROMPT_LOCALE }, { "prompt-keymap", no_argument, NULL, ARG_PROMPT_KEYMAP }, + { "prompt-keymap-auto", no_argument, NULL, ARG_PROMPT_KEYMAP_AUTO }, { "prompt-timezone", no_argument, NULL, ARG_PROMPT_TIMEZONE }, { "prompt-hostname", no_argument, NULL, ARG_PROMPT_HOSTNAME }, { "prompt-root-password", no_argument, NULL, ARG_PROMPT_ROOT_PASSWORD }, @@ -1480,6 +1499,7 @@ static int parse_argv(int argc, char *argv[]) { case ARG_PROMPT: arg_prompt_locale = arg_prompt_keymap = arg_prompt_timezone = arg_prompt_hostname = arg_prompt_root_password = arg_prompt_root_shell = true; + arg_prompt_keymap_auto = false; break; case ARG_PROMPT_LOCALE: @@ -1488,6 +1508,11 @@ static int parse_argv(int argc, char *argv[]) { case ARG_PROMPT_KEYMAP: arg_prompt_keymap = true; + arg_prompt_keymap_auto = false; + break; + + case ARG_PROMPT_KEYMAP_AUTO: + arg_prompt_keymap_auto = true; break; case ARG_PROMPT_TIMEZONE: @@ -1668,7 +1693,7 @@ static int run(int argc, char *argv[]) { return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m"); if (r > 0 && !enabled) { log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts."); - arg_prompt_locale = arg_prompt_keymap = arg_prompt_timezone = arg_prompt_hostname = arg_prompt_root_password = arg_prompt_root_shell = false; + arg_prompt_locale = arg_prompt_keymap = arg_prompt_keymap_auto = arg_prompt_timezone = arg_prompt_hostname = arg_prompt_root_password = arg_prompt_root_shell = false; } } diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 5f7209231a..d6946ba4ee 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -221,6 +221,10 @@ grep -q "LC_MESSAGES=bar" "$ROOT$LOCALE_PATH" if [ -d "/usr/share/keymaps/" ] || [ -d "/usr/share/kbd/keymaps/" ] || [ -d "/usr/lib/kbd/keymaps/" ] ; then echo -ne "foo\n" | systemd-firstboot --root="$ROOT" --prompt-keymap grep -q "KEYMAP=foo" "$ROOT/etc/vconsole.conf" + + rm "$ROOT/etc/vconsole.conf" + # this should be a NOP, given that stdout is connected to /dev/null, and hence not a VT + systemd-firstboot --root="$ROOT" --prompt-keymap-auto > /dev/null fi echo -ne "Europe/Berlin\n" | systemd-firstboot --root="$ROOT" --prompt-timezone readlink "$ROOT/etc/localtime" | grep -q "Europe/Berlin$" diff --git a/units/systemd-firstboot.service b/units/systemd-firstboot.service index 5ba9c8ba03..fb1c59ad6f 100644 --- a/units/systemd-firstboot.service +++ b/units/systemd-firstboot.service @@ -32,7 +32,7 @@ Before=shutdown.target [Service] Type=oneshot RemainAfterExit=yes -ExecStart=systemd-firstboot --prompt-locale --prompt-keymap --prompt-timezone --prompt-root-password --mute-console=yes +ExecStart=systemd-firstboot --prompt-locale --prompt-keymap-auto --prompt-timezone --prompt-root-password --mute-console=yes StandardOutput=tty StandardInput=tty StandardError=tty From 372f3159fd6f34fd657e44b473f159dcc54e7a06 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Sep 2025 15:55:49 +0200 Subject: [PATCH 8/8] firstboot: don't talk about any 'new' installation The firstboot interface might pop-up also during live mode (because it's just very useful to have the right kbd mapping applied), and that's hardly a "new installation". Hence use more generic terms --- src/firstboot/firstboot.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 9f237dc266..4c24d38716 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -139,16 +139,16 @@ static void print_welcome(int rfd, sd_varlink **mute_console_link) { ac = isempty(ansi_color) ? "0" : ansi_color; if (colors_enabled()) - printf(ANSI_HIGHLIGHT "Welcome to your new installation of " ANSI_NORMAL "\x1B[%sm%s" ANSI_HIGHLIGHT "!" ANSI_NORMAL "\n", ac, pn); + printf(ANSI_HIGHLIGHT "Welcome to " ANSI_NORMAL "\x1B[%sm%s" ANSI_HIGHLIGHT "!" ANSI_NORMAL "\n", ac, pn); else - printf("Welcome to your new installation of %s!\n", pn); + printf("Welcome to %s!\n", pn); putchar('\n'); if (emoji_enabled()) { fputs(glyph(GLYPH_SPARKLES), stdout); putchar(' '); } - printf("Please configure your new system!\n\n"); + printf("Please configure the system!\n\n"); done = true; }