From: Michael Vogt Date: Fri, 12 Jun 2026 10:43:15 +0000 (+0200) Subject: creds,firstboot: add support for ? and $ via credentials X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=513e8a382652cc4d35690d472f54e399033810cc;p=thirdparty%2Fsystemd.git creds,firstboot: add support for ? and $ via credentials Now that we support the `$` we want to also make this available inside the system.hostname and firstboot.hostname credentials and the firstboot --hostname option. This commit adds it (and also `?`). --- diff --git a/man/hostname.xml b/man/hostname.xml index 04d6769411a..708581c841d 100644 --- a/man/hostname.xml +++ b/man/hostname.xml @@ -79,6 +79,20 @@ ? characters and separators) stays within this limit. An expanded name that exceeds the limit is considered invalid and the built-in fallback hostname is used. + Because the name only stays stable as long as the pattern and word lists are unchanged, the + preferred way to obtain a stable, generated hostname is to provide the pattern through the + firstboot.hostname credential (see + systemd.system-credentials7) + rather than placing it directly in this file. Concretely, write the pattern to + /etc/credstore/firstboot.hostname (see the description of + LoadCredential= in + systemd.exec5 for the + credential store): when firstboot runs on the live system the credential is resolved once and the + concrete result is persisted here, so the word lists may be updated afterwards without changing the + hostname. A pattern placed directly in /etc/hostname (or provisioned into an offline + image) is instead re-evaluated on every boot, which only really makes sense for systems that keep the + wordlist and the pattern stable. + You may use diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index f316961cd72..eba06bc9ca2 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -60,7 +60,20 @@ hostname, and only has an effect on first boot, unlike system.hostname (see below). Read by systemd-firstboot1 - and only honoured if no static hostname has been configured before. + and only honoured if no static hostname has been configured before. The value may use the wildcard + patterns documented in + hostname5 (e.g. + $-$ or foo-????). When the credential is applied on the running + system (the usual case, by systemd-firstboot.service on first boot), the wildcards + are resolved against the machine ID and the resulting concrete name is written to + /etc/hostname. The name is thus persisted once at first boot and + does not change on subsequent boots even if the word lists are later updated or the pattern is changed + (for example to add more ?/$ tokens for additional entropy), which is the recommended + way to obtain a stable, per-machine generated hostname. (When + systemd-firstboot1 + operates on an offline image via /, the target's + machine ID is not yet known, so the pattern is written verbatim and, like a pattern placed directly in + /etc/hostname, re-derived on every boot rather than persisted.) @@ -430,7 +443,15 @@ in /etc/hostname, if configured, takes precedence over this setting. Interpreted by the service manager (PID 1). For details see systemd1. Also - see firstboot.hostname above. + see firstboot.hostname above. The value may use the wildcard patterns documented + in hostname5 + (? and $), which are expanded deterministically from the + machine ID when the credential is applied. As this is a transient hostname that is re-applied on + every boot, a wildcard value is re-derived each boot (and may change if the word lists change) and + is never persisted; pass an already-resolved name if a stable hostname is required. Note also that + the word-list tokens require the word lists to be present, which is generally not the case in the + initrd so during initrd the default hostname is used and in late boot the resolved one becomes + available. diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index e922abdd63c..2200235e1d1 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -28,6 +28,7 @@ #include "fs-util.h" #include "glyph-util.h" #include "help-util.h" +#include "hostname-setup.h" #include "hostname-util.h" #include "image-policy.h" #include "kbd-util.h" @@ -676,7 +677,7 @@ static int prompt_hostname(int rfd, sd_varlink **mute_console_link) { r = read_credential("firstboot.hostname", (void**) &hn, NULL); if (r < 0) log_debug_errno(r, "Failed to read credential firstboot.hostname, ignoring: %m"); - else if (!hostname_is_valid(hn, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK)) + else if (!hostname_is_valid(hn, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN)) log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Hostname '%s' supplied via credential is not valid, ignoring.", hn); else { log_debug("Acquired hostname from credentials."); @@ -738,7 +739,23 @@ static int process_hostname(int rfd, sd_varlink **mute_console_link) { if (isempty(arg_hostname)) return 0; - r = write_string_file_at(pfd, f, arg_hostname, + /* On running systems we have a machine ID, so resolve any '?'/'$' wildcards now and persist them. + * This "freezes" the name, so later word list updates do not change it. When operating on an offline + * image (--root=/--image=) the target's machine ID is not known yet, so write the template verbatim + * and let it be resolved on each first boot. */ + const char *hostname = arg_hostname; + _cleanup_free_ char *resolved = NULL; + if (!arg_root) { + r = hostname_substitute_wildcards(arg_hostname, &resolved); + if (r < 0) + log_warning_errno(r, "Failed to resolve wildcards in hostname '%s', writing it verbatim: %m", arg_hostname); + else if (!hostname_is_valid(resolved, VALID_HOSTNAME_TRAILING_DOT)) + log_warning("Resolved hostname '%s' is invalid, writing template '%s' verbatim instead.", resolved, arg_hostname); + else + hostname = resolved; + } + + r = write_string_file_at(pfd, f, hostname, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL); if (r < 0) return log_error_errno(r, "Failed to write /etc/hostname: %m"); @@ -1407,7 +1424,7 @@ static int parse_argv(int argc, char *argv[]) { break; OPTION_LONG("hostname", "NAME", "Set hostname"): - if (!hostname_is_valid(opts.arg, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK)) + if (!hostname_is_valid(opts.arg, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Host name %s is not valid.", opts.arg); diff --git a/src/shared/hostname-setup.c b/src/shared/hostname-setup.c index 4a1850ae289..63464f26cfc 100644 --- a/src/shared/hostname-setup.c +++ b/src/shared/hostname-setup.c @@ -96,11 +96,19 @@ static int acquire_hostname_from_credential(char **ret) { if (r == 0) /* not found */ return -ENXIO; - if (!hostname_is_valid(cred, VALID_HOSTNAME_TRAILING_DOT)) /* check that the hostname we return is valid */ + if (!hostname_is_valid(cred, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN)) return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname specified in system.hostname credential is invalid, ignoring: %s", cred); + _cleanup_free_ char *substituted = NULL; + r = hostname_substitute_wildcards(cred, &substituted); + if (r < 0) + return log_warning_errno(r, "Failed to substitute wildcards in system.hostname credential, ignoring: %m"); + + if (!hostname_is_valid(substituted, VALID_HOSTNAME_TRAILING_DOT)) /* check that the expanded hostname is valid */ + return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname from system.hostname credential is invalid after expansion, ignoring: %s", substituted); + log_info("Initializing hostname from credential."); - *ret = TAKE_PTR(cred); + *ret = TAKE_PTR(substituted); return 0; } diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 557bd3af012..8c3c222c884 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -11,6 +11,14 @@ if ! command -v systemd-firstboot >/dev/null; then exit 0 fi +restore_etc_hostname() { + if [[ -e /tmp/etc-hostname.bak ]]; then + mv /tmp/etc-hostname.bak /etc/hostname + elif [[ -e /tmp/etc-hostname.absent ]]; then + rm -f /etc/hostname /tmp/etc-hostname.absent + fi +} + at_exit() { if [[ -n "${ROOT:-}" ]]; then ls -lR "$ROOT" @@ -22,6 +30,11 @@ at_exit() { rm -rf /etc/otherpath fi + [[ -n "${WROOT:-}" ]] && rm -rf "$WROOT" + [[ -n "${WCREDS:-}" ]] && rm -rf "$WCREDS" + [[ -n "${WWORDS:-}" ]] && rm -rf "$WWORDS" + restore_etc_hostname + restore_locale } @@ -82,6 +95,57 @@ readlink "$ROOT/etc/localtime" | grep "Europe/Berlin" >/dev/null systemd-firstboot --root="$ROOT" --hostname "foobar" grep -q "foobar" "$ROOT/etc/hostname" +# Hostname wildcard templates (see hostname(5)) must be accepted by both the --hostname option and the +# firstboot.hostname credential (both go through hostname_is_valid() with the wildcard flags). Since these +# invocations operate on an offline image (--root=), the template is written verbatim and only resolved +# later, on the target's first boot (the resolve-and-freeze path is taken only when running on the live +# system, where the machine ID is final). +WROOT=test-root-hostname-wildcard + +# '$' word token via --hostname, stored as-is +rm -rf "$WROOT"; mkdir -p "$WROOT" +systemd-firstboot --root="$WROOT" --hostname='$-$' +assert_eq "$(cat "$WROOT/etc/hostname")" '$-$' + +# '?' hex token via --hostname, stored as-is +rm -rf "$WROOT"; mkdir -p "$WROOT" +systemd-firstboot --root="$WROOT" --hostname='foo-????' +assert_eq "$(cat "$WROOT/etc/hostname")" 'foo-????' + +# the firstboot.hostname credential is accepted and written verbatim too +rm -rf "$WROOT"; mkdir -p "$WROOT" +WCREDS="$(mktemp -d)" +echo -n '$-$-????' >"$WCREDS/firstboot.hostname" +CREDENTIALS_DIRECTORY="$WCREDS" systemd-firstboot --root="$WROOT" +assert_eq "$(cat "$WROOT/etc/hostname")" '$-$-????' +rm -rf "$WCREDS" + +# an invalid hostname (disallowed character) is refused and nothing is written +rm -rf "$WROOT"; mkdir -p "$WROOT" +(! systemd-firstboot --root="$WROOT" --hostname='foo_bar') +[[ ! -e "$WROOT/etc/hostname" ]] + +rm -rf "$WROOT" + +# When run on the live system (no --root=) the machine ID is final, so the wildcards are resolved +# immediately and the concrete result is persisted, "freezing" the generated name (see hostname(5)). +if [[ -f /etc/hostname ]]; then + cp /etc/hostname /tmp/etc-hostname.bak +else + touch /tmp/etc-hostname.absent +fi +WWORDS="$(mktemp -d)" +printf 'wildly\nquietly\n' >"$WWORDS/1" +printf 'happy\nsad\n' >"$WWORDS/2" +SYSTEMD_HOSTNAME_WORDLIST_PATH="$WWORDS" systemd-firstboot --force --hostname='$-$-????' +H="$(cat /etc/hostname)" +IFS='-' read -r w1 w2 suffix <<<"$H" +grep -Fx -- "$w1" "$WWORDS/1" >/dev/null +grep -Fx -- "$w2" "$WWORDS/2" >/dev/null +[[ "$suffix" =~ ^[0-9a-f]{4}$ ]] +rm -rf "$WWORDS" +restore_etc_hostname + systemd-firstboot --root="$ROOT" --machine-id=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa grep -q "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "$ROOT/etc/machine-id"