firstboot: add new systemd.firstboot=headless mode

This adds a new systemd.firstboot=headless mode. It differs from the
existing systemd.firstboot=no mode in that it still performs the
non-interactive auto-configuration that requires no user input (such
as selecting the only installed locale, or applying settings provided
via credentials), and only skips the prompts that would otherwise
block waiting for user input. In contrast, =no disables that
auto-configuration along with the prompts.

The option is also honoured by homectls firstboot logic and
systemd-cryptenroll, where headless behaves the same as no
(because the is no auto-configuration).
This commit is contained in:
Michael Vogt
2026-06-30 18:08:47 +02:00
committed by Luca Boccassi
parent 03b498d8a8
commit 896fd702ba
12 changed files with 177 additions and 25 deletions

11
NEWS
View File

@@ -16,6 +16,17 @@ CHANGES WITH 262:
have been provided. This clears the way for a new
"systemd-sysupdate@.service" unit for varlink activation of sysupdate.
Changes in systemd-firstboot:
* The "systemd.firstboot=" kernel command line option now accepts the
special value "headless" in addition to a boolean. Like "no", it
suppresses all interactive prompts, but unlike "no" it still performs
non-interactive auto-configuration that requires no user input (such
as selecting the sole installed locale, or applying settings provided
via credentials). This is useful for unattended installations that
should be provisioned as far as possible without ever blocking on a
prompt.
CHANGES WITH 261:
Announcements of Future Feature Removals and Incompatible Changes:

View File

@@ -645,15 +645,20 @@
<varlistentry>
<term><varname>systemd.firstboot=</varname></term>
<listitem><para>Takes a boolean argument, defaults to on. If off,
<listitem><para>Takes a boolean argument or the special value <literal>headless</literal>, defaults to
on. If off,
<citerefentry><refentrytitle>systemd-firstboot.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and
<citerefentry><refentrytitle>systemd-homed-firstboot.service</refentrytitle><manvolnum>1</manvolnum></citerefentry>
will not query the user for basic system settings, even if the system boots up for the first time and
the relevant settings are not initialized yet. Not to be confused with
<varname>systemd.condition_first_boot=</varname> (see below), which overrides the result of the
<varname>ConditionFirstBoot=</varname> unit file condition, and thus controls more than just
<filename>systemd-firstboot.service</filename> behaviour.</para>
the relevant settings are not initialized yet. If set to <literal>headless</literal>, the same
interactive prompts are suppressed as with <literal>no</literal>, but non-interactive
auto-configuration that requires no user input is still performed (for example, selecting the only
installed locale, or applying settings provided via credentials). This is useful for unattended
installations that should be provisioned as far as possible without ever blocking on a prompt. Not to
be confused with <varname>systemd.condition_first_boot=</varname> (see below), which overrides the
result of the <varname>ConditionFirstBoot=</varname> unit file condition, and thus controls more than
just <filename>systemd-firstboot.service</filename> behaviour.</para>
<xi:include href="version-info.xml" xpointer="v233"/></listitem>
</varlistentry>

View File

@@ -521,9 +521,12 @@
<varlistentry>
<term><varname>systemd.firstboot=</varname></term>
<listitem><para>Takes a boolean argument, defaults to on. If off, <filename>systemd-firstboot.service</filename>
will not interactively query the user for basic settings at first boot, even if those settings are not
initialized yet.</para>
<listitem><para>Takes a boolean argument or the special value <literal>headless</literal>, defaults to
on. If off, <filename>systemd-firstboot.service</filename> will not interactively query the user for
basic settings at first boot, even if those settings are not initialized yet. If set to
<literal>headless</literal>, interactive prompts are suppressed just like with <literal>no</literal>,
but non-interactive auto-configuration that requires no user input is still performed (for example,
selecting the sole installed locale, or applying settings passed in via credentials).</para>
<xi:include href="version-info.xml" xpointer="v233"/></listitem>
</varlistentry>

View File

@@ -8,10 +8,10 @@
#include "cryptenroll-interactive.h"
#include "cryptenroll-list.h"
#include "cryptsetup-util.h"
#include "firstboot-util.h"
#include "glyph-util.h"
#include "libfido2-util.h"
#include "log.h"
#include "proc-cmdline.h"
#include "prompt-util.h"
#include "string-util.h"
#include "strv.h"
@@ -176,12 +176,15 @@ int cryptenroll_run_interactive(
assert(c->node);
/* Honour the systemd.firstboot= kernel command line option, just like systemd-firstboot. */
bool enabled;
r = proc_cmdline_get_bool("systemd.firstboot", PROC_CMDLINE_TRUE_WHEN_MISSING, &enabled);
FirstBootMode mode;
_cleanup_free_ char *bad = NULL;
r = firstboot_mode_from_cmdline(&mode, &bad);
if (r < 0)
log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring: %m");
else if (!enabled) {
log_debug("systemd.firstboot=no set, skipping interactive enrollment.");
log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring%s: %m",
bad ? strjoina(" (invalid value '", bad, "')") : "");
else if (IN_SET(mode, FIRSTBOOT_NO, FIRSTBOOT_HEADLESS)) {
log_debug("systemd.firstboot=%s set, skipping interactive enrollment.",
firstboot_mode_to_string(mode));
return 0;
}

View File

@@ -24,6 +24,7 @@
#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "firstboot-util.h"
#include "format-table.h"
#include "fs-util.h"
#include "glyph-util.h"
@@ -47,7 +48,6 @@
#include "password-quality-util.h"
#include "path-util.h"
#include "plymouth-util.h"
#include "proc-cmdline.h"
#include "prompt-util.h"
#include "runtime-scope.h"
#include "smack-util.h"
@@ -79,6 +79,7 @@ static bool arg_prompt_timezone = false;
static bool arg_prompt_hostname = false;
static bool arg_prompt_root_password = false;
static bool arg_prompt_root_shell = false;
static bool arg_headless = false;
static bool arg_copy_locale = false;
static bool arg_copy_keymap = false;
static bool arg_copy_timezone = false;
@@ -244,6 +245,16 @@ static int locale_is_ok(const char *name, void *userdata) {
return r != 0 ? locale_is_installed(name) > 0 : locale_is_valid(name);
}
static bool headless_skips_prompt_for(const char *what) {
assert(what);
if (!arg_headless)
return false;
log_debug("Running headless, not prompting for %s.", what);
return true;
}
static int prompt_locale(int rfd, sd_varlink **mute_console_link) {
_cleanup_strv_free_ char **locales = NULL;
bool acquired_from_creds = false;
@@ -296,6 +307,9 @@ static int prompt_locale(int rfd, sd_varlink **mute_console_link) {
/* Not setting arg_locale_message here, since it defaults to LANG anyway */
}
} else {
if (headless_skips_prompt_for("locale"))
return 0;
print_welcome(rfd, mute_console_link);
_cleanup_free_ char *prefill = NULL;
@@ -456,6 +470,9 @@ static int prompt_keymap(int rfd, sd_varlink **mute_console_link) {
return 0;
}
if (headless_skips_prompt_for("keymap"))
return 0;
r = get_keymaps(&kmaps);
if (r == -ENOENT) /* no keymaps installed */
return log_debug_errno(r, "No keymaps are installed.");
@@ -578,6 +595,9 @@ static int prompt_timezone(int rfd, sd_varlink **mute_console_link) {
return 0;
}
if (headless_skips_prompt_for("timezone"))
return 0;
r = get_timezones(&zones);
if (r < 0)
return log_error_errno(r, "Cannot query timezone list: %m");
@@ -691,6 +711,9 @@ static int prompt_hostname(int rfd, sd_varlink **mute_console_link) {
return 0;
}
if (headless_skips_prompt_for("hostname"))
return 0;
print_welcome(rfd, mute_console_link);
r = prompt_loop("Please enter the new hostname",
@@ -879,6 +902,9 @@ static int prompt_root_password(int rfd, sd_varlink **mute_console_link) {
return 0;
}
if (headless_skips_prompt_for("root password"))
return 0;
print_welcome(rfd, mute_console_link);
msg1 = "Please enter the new root password (empty to skip):";
@@ -980,6 +1006,9 @@ static int prompt_root_shell(int rfd, sd_varlink **mute_console_link) {
return 0;
}
if (headless_skips_prompt_for("root shell"))
return 0;
print_welcome(rfd, mute_console_link);
return prompt_loop(
@@ -1688,13 +1717,18 @@ static int run(int argc, char *argv[]) {
* command line option, because we are called to provision the host with basic settings (as
* opposed to some other file system tree/image) */
bool enabled;
r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled);
FirstBootMode mode;
_cleanup_free_ char *bad = NULL;
r = firstboot_mode_from_cmdline(&mode, &bad);
if (r < 0)
return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
if (r > 0 && !enabled) {
return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument%s: %m",
bad ? strjoina(" (invalid value '", bad, "')") : "");
if (mode == FIRSTBOOT_NO) {
log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
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;
} else if (mode == FIRSTBOOT_HEADLESS) {
log_debug("Found systemd.firstboot=headless kernel command line argument, skipping interactive prompts but keeping non-interactive auto-configuration.");
arg_headless = true;
}
}

View File

@@ -25,6 +25,7 @@
#include "extract-word.h"
#include "fd-util.h"
#include "fileio.h"
#include "firstboot-util.h"
#include "format-table.h"
#include "format-util.h"
#include "fs-util.h"
@@ -52,7 +53,6 @@
#include "pkcs11-util.h"
#include "plymouth-util.h"
#include "polkit-agent.h"
#include "proc-cmdline.h"
#include "process-util.h"
#include "prompt-util.h"
#include "recurse-dir.h"
@@ -3120,12 +3120,15 @@ static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdat
/* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
* tool. */
bool enabled;
r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled);
FirstBootMode mode;
_cleanup_free_ char *bad = NULL;
r = firstboot_mode_from_cmdline(&mode, &bad);
if (r < 0)
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.");
return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument%s: %m",
bad ? strjoina(" (invalid value '", bad, "')") : "");
if (IN_SET(mode, FIRSTBOOT_NO, FIRSTBOOT_HEADLESS)) {
log_debug("Found systemd.firstboot=%s kernel command line argument, turning off all prompts.",
firstboot_mode_to_string(mode));
arg_prompt_new_user = false;
}

View File

@@ -0,0 +1,45 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "firstboot-util.h"
#include "proc-cmdline.h"
#include "string-table.h"
static const char* const firstboot_mode_table[_FIRSTBOOT_MODE_MAX] = {
[FIRSTBOOT_NO] = "no",
[FIRSTBOOT_INTERACTIVE] = "interactive",
[FIRSTBOOT_HEADLESS] = "headless",
};
assert_cc(FIRSTBOOT_NO == 0);
DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(firstboot_mode, FirstBootMode, FIRSTBOOT_INTERACTIVE);
int firstboot_mode_from_cmdline(FirstBootMode *ret, char **reterr_value) {
_cleanup_free_ char *value = NULL;
int r;
assert(ret);
r = proc_cmdline_get_key("systemd.firstboot", PROC_CMDLINE_VALUE_OPTIONAL, &value);
if (r < 0)
return r;
if (r == 0) { /* not specified at all */
*ret = FIRSTBOOT_INTERACTIVE;
return 0;
}
if (!value) { /* key without parameter, i.e. bare "systemd.firstboot" */
*ret = FIRSTBOOT_INTERACTIVE;
return 1;
}
FirstBootMode m = firstboot_mode_from_string(value);
if (m < 0) {
if (reterr_value)
*reterr_value = TAKE_PTR(value);
return m;
}
*ret = m;
return 1;
}

View File

@@ -0,0 +1,18 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "shared-forward.h"
/* Parsed value of the systemd.firstboot= kernel command line option, honoured by systemd-firstboot,
* homectl's firstboot logic and systemd-cryptenroll. */
typedef enum FirstBootMode {
FIRSTBOOT_NO, /* "no": don't prompt, don't auto-configure */
FIRSTBOOT_INTERACTIVE, /* "interactive" or unset: prompt as needed */
FIRSTBOOT_HEADLESS, /* "headless": auto-configure, but never prompt */
_FIRSTBOOT_MODE_MAX,
_FIRSTBOOT_MODE_INVALID = -EINVAL,
} FirstBootMode;
DECLARE_STRING_TABLE_LOOKUP(firstboot_mode, FirstBootMode);
int firstboot_mode_from_cmdline(FirstBootMode *ret, char **reterr_value);

View File

@@ -85,6 +85,7 @@ shared_sources = files(
'fido2-util.c',
'find-esp.c',
'firewall-util.c',
'firstboot-util.c',
'fork-notify.c',
'format-table.c',
'fsprg-openssl.c',

View File

@@ -108,6 +108,7 @@ simple_tests += files(
'test-fiemap.c',
'test-fileio.c',
'test-firewall-util.c',
'test-firstboot-util.c',
'test-format-table.c',
'test-format-util.c',
'test-fs-util.c',

View File

@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "firstboot-util.h"
#include "tests.h"
TEST(firstboot_mode_from_string) {
assert_se(firstboot_mode_from_string("yes") == FIRSTBOOT_INTERACTIVE);
assert_se(firstboot_mode_from_string("1") == FIRSTBOOT_INTERACTIVE);
assert_se(firstboot_mode_from_string("on") == FIRSTBOOT_INTERACTIVE);
assert_se(firstboot_mode_from_string("true") == FIRSTBOOT_INTERACTIVE);
assert_se(firstboot_mode_from_string("interactive") == FIRSTBOOT_INTERACTIVE);
assert_se(firstboot_mode_from_string("no") == FIRSTBOOT_NO);
assert_se(firstboot_mode_from_string("0") == FIRSTBOOT_NO);
assert_se(firstboot_mode_from_string("off") == FIRSTBOOT_NO);
assert_se(firstboot_mode_from_string("false") == FIRSTBOOT_NO);
assert_se(firstboot_mode_from_string("headless") == FIRSTBOOT_HEADLESS);
assert_se(firstboot_mode_from_string("") == _FIRSTBOOT_MODE_INVALID);
assert_se(firstboot_mode_from_string(NULL) == _FIRSTBOOT_MODE_INVALID);
assert_se(firstboot_mode_from_string("Headless") == _FIRSTBOOT_MODE_INVALID);
assert_se(firstboot_mode_from_string("maybe") == _FIRSTBOOT_MODE_INVALID);
}
DEFINE_TEST_MAIN(LOG_INFO);

View File

@@ -12,6 +12,7 @@
#include "device-private.h"
#include "discover-image.h"
#include "execute.h"
#include "firstboot-util.h"
#include "gpt.h"
#include "import-util.h"
#include "install.h"
@@ -62,6 +63,7 @@ int main(int argc, char **argv) {
test_table(ExecOutput, exec_output, EXEC_OUTPUT);
test_table(ExecPreserveMode, exec_preserve_mode, EXEC_PRESERVE_MODE);
test_table(ExecUtmpMode, exec_utmp_mode, EXEC_UTMP_MODE);
test_table(FirstBootMode, firstboot_mode, FIRSTBOOT_MODE);
test_table(ImageType, image_type, IMAGE_TYPE);
test_table(ImportVerify, import_verify, IMPORT_VERIFY);
test_table(JobMode, job_mode, JOB_MODE);