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 `?`).
This commit is contained in:
Michael Vogt
2026-06-20 14:26:41 +02:00
parent df12d58f8b
commit 513e8a3826
5 changed files with 131 additions and 7 deletions
+14
View File
@@ -79,6 +79,20 @@
<literal>?</literal> 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.</para>
<para>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
<varname>firstboot.hostname</varname> credential (see
<citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry>)
rather than placing it directly in this file. Concretely, write the pattern to
<filename>/etc/credstore/firstboot.hostname</filename> (see the description of
<varname>LoadCredential=</varname> in
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> 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 <filename>/etc/hostname</filename> (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.</para>
<xi:include href="version-info.xml" xpointer="v262"/>
<para>You may use
+23 -2
View File
@@ -60,7 +60,20 @@
hostname, and only has an effect on first boot, unlike <varname>system.hostname</varname> (see
below). Read by
<citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>
and only honoured if no static hostname has been configured before.</para>
and only honoured if no static hostname has been configured before. The value may use the wildcard
patterns documented in
<citerefentry><refentrytitle>hostname</refentrytitle><manvolnum>5</manvolnum></citerefentry> (e.g.
<literal>$-$</literal> or <literal>foo-????</literal>). When the credential is applied on the running
system (the usual case, by <filename>systemd-firstboot.service</filename> on first boot), the wildcards
are resolved against the machine ID and the resulting concrete name is written to
<filename>/etc/hostname</filename>. The name is thus <emphasis>persisted</emphasis> 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 <literal>?</literal>/<literal>$</literal> tokens for additional entropy), which is the recommended
way to obtain a stable, per-machine generated hostname. (When
<citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>
operates on an offline image via <option>--root=</option>/<option>--image=</option>, the target's
machine ID is not yet known, so the pattern is written verbatim and, like a pattern placed directly in
<filename>/etc/hostname</filename>, re-derived on every boot rather than persisted.)</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
@@ -430,7 +443,15 @@
in <filename>/etc/hostname</filename>, if configured, takes precedence over this setting.
Interpreted by the service manager (PID 1). For details see
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>. Also
see <varname>firstboot.hostname</varname> above.</para>
see <varname>firstboot.hostname</varname> above. The value may use the wildcard patterns documented
in <citerefentry><refentrytitle>hostname</refentrytitle><manvolnum>5</manvolnum></citerefentry>
(<literal>?</literal> and <literal>$</literal>), 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.</para>
<xi:include href="version-info.xml" xpointer="v254"/>
</listitem>
+20 -3
View File
@@ -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);
+10 -2
View File
@@ -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;
}
+64
View File
@@ -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"