]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
credentials: add policy that can allow key=null creds from the ESP
authorMichael Vogt <michael@amutable.com>
Wed, 10 Jun 2026 11:02:01 +0000 (13:02 +0200)
committerMichael Vogt <michael@amutable.com>
Thu, 9 Jul 2026 14:48:26 +0000 (16:48 +0200)
This commit adds a new `systemd.credentials_boot_policy=` kernel
commandline that allows to control if credentials with
a `null` key are accepted.

The possible options are:
* strict: always insist on tpm encryption
* tofu: allow null encryption in firstboot mode and when no tpm is available
* relaxed: allow null encryption when sb is off, or no tpm is available
* off: allow null encryption always

The default is currently `relaxed` which is the same behavior
as before.

This replaces the initial idea of using plaintext credentials
at firstboot (thanks to Lennart for this nicer and simpler design).

Note that this also moves `in_first_boot()` to `basic/initrd-util`
which is a better fit now.

docs/CREDENTIALS.md
man/kernel-command-line.xml
man/systemd.xml
src/basic/initrd-util.c
src/basic/initrd-util.h
src/shared/condition.c
src/shared/creds-util.c
src/shared/creds-util.h
src/test/test-creds.c
test/units/TEST-54-CREDS.sh

index 149cf2f7db27427aa4e92aaaca7c2bd3b877052e..5260db6b33281057ee40b70d3bea4797f1766c70 100644 (file)
@@ -274,6 +274,33 @@ SetCredentialEncrypted=foobar: \
 …
 ```
 
+### Null-Key Encryption and the Boot Policy
+
+In some situations a credential must be provisioned to a machine that does not
+yet (or does not at all) possess a key to encrypt against, for example before
+first boot completes, or on systems without a TPM2 device. For these cases
+`systemd-creds encrypt --with-key=null` produces a credential wrapped in the
+normal `systemd-creds` envelope, but encrypted with a fixed zero-length key.
+Such a credential travels through the same path as any other encrypted
+credential (e.g. it may be dropped into the ESP under `/loader/credentials/` or
+`*.efi.extra.d/`), but it offers neither confidentiality nor authenticity.
+Because of that, accepting it is risky on a system that could do better, and
+whether it is accepted at boot is controlled by the `systemd.credentials_boot_policy=`
+kernel command line option:
+
+| Mode      | A null-key credential is accepted when…           |
+|-----------|---------------------------------------------------|
+| `strict`  | never                                             |
+| `tofu`    | this is the first boot, or no TPM2 is available   |
+| `relaxed` | SecureBoot is off, or no TPM2 is available        |
+| `off`     | always                                            |
+
+The default is `relaxed`. This policy only governs the
+default acceptance decision; credentials encrypted against a host key or TPM2
+device are always accepted. See
+[systemd(1)](https://www.freedesktop.org/software/systemd/man/latest/systemd.html)
+for details.
+
 ## Inheritance from Container Managers, Hypervisors, Kernel Command Line, or the UEFI Boot Environment
 
 Sometimes it is useful to parameterize whole systems the same way as services,
index 768c7653fb4113b228a817316531d7581553ae05..9134c987c9a5d7b391a27955e31bdc71985fc46a 100644 (file)
@@ -67,6 +67,7 @@
         <term><varname>systemd.set_credential=</varname></term>
         <term><varname>systemd.set_credential_binary=</varname></term>
         <term><varname>systemd.import_credentials=</varname></term>
+        <term><varname>systemd.credentials_boot_policy=</varname></term>
         <term><varname>systemd.reload_limit_interval_sec=</varname></term>
         <term><varname>systemd.reload_limit_burst=</varname></term>
         <term><varname>systemd.minimum_uptime_sec=</varname></term>
index 557cd76648212375279bcc6f24792b5f7116bb1a..51ce1854fbbe54d674fd526e5c21e4dd889e429d 100644 (file)
         <xi:include href="version-info.xml" xpointer="v251"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>systemd.credentials_boot_policy=</varname></term>
+
+        <listitem><para>Controls under which conditions a credential encrypted with a <literal>null</literal>
+        key (i.e. a credential wrapped in a <command>systemd-creds</command> envelope but offering neither
+        confidentiality nor authenticity, see
+        <citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
+        is accepted at boot. This provides a fallback for provisioning credentials before a machine has a key
+        to encrypt them against, for example during first boot or on systems without a TPM2 device. Takes
+        one of <option>strict</option>, <option>tofu</option>, <option>relaxed</option> or
+        <option>off</option>:</para>
+
+        <itemizedlist>
+          <listitem><para><option>strict</option> never accepts a <literal>null</literal> key, i.e. always
+          insists on proper encryption.</para></listitem>
+
+          <listitem><para><option>tofu</option> (trust on first use) accepts a <literal>null</literal> key
+          during first boot, or when no TPM2 device is available.</para></listitem>
+
+          <listitem><para><option>relaxed</option> accepts a <literal>null</literal> key when SecureBoot is
+          disabled, or when no TPM2 device is available.</para></listitem>
+
+          <listitem><para><option>off</option> always accepts a <literal>null</literal> key.</para></listitem>
+        </itemizedlist>
+
+        <para>Defaults to <option>relaxed</option>. Note that this policy only applies to the default acceptance
+        decision; it has no effect on credentials encrypted against a host key or TPM2 device, which are
+        always accepted.</para>
+
+        <para>For further information see <ulink url="https://systemd.io/CREDENTIALS">System and Service
+        Credentials</ulink> documentation.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>quiet</varname></term>
 
index c45920b3070ae896573d3a5ee9b0f8645fac3d44..fc497f59f5ee9a0cc35f8805d8b7c1487c60d58b 100644 (file)
@@ -1,11 +1,13 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <stdlib.h>
 #include <unistd.h>
 
 #include "env-util.h"
 #include "errno-util.h"
 #include "initrd-util.h"
 #include "log.h"
+#include "parse-util.h"
 
 static int saved_in_initrd = -1;
 
@@ -38,3 +40,25 @@ bool in_initrd(void) {
 void in_initrd_force(bool value) {
         saved_in_initrd = value;
 }
+
+bool in_first_boot(void) {
+        static int first_boot = -1;
+        int r;
+
+        if (first_boot >= 0)
+                return first_boot;
+
+        const char *e = secure_getenv("SYSTEMD_FIRST_BOOT");
+        if (e) {
+                r = parse_boolean(e);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to parse $SYSTEMD_FIRST_BOOT, ignoring: %m");
+                else
+                        return (first_boot = r);
+        }
+
+        r = RET_NERRNO(access("/run/systemd/first-boot", F_OK));
+        if (r < 0 && r != -ENOENT)
+                log_debug_errno(r, "Failed to check if /run/systemd/first-boot exists, assuming no: %m");
+        return r >= 0;
+}
index 387c2c6717cbe981d9c76bc1171582343142be47..ec55ec34028a9fc95b712ad8e3577a5708c10bbe 100644 (file)
@@ -5,3 +5,5 @@
 
 bool in_initrd(void);
 void in_initrd_force(bool value);
+
+bool in_first_boot(void);
index 2b4a29ed5c60d204109a58bd6b265175fac4deae..be2754c15eb0b886d2406f1eebaa43ac81c715ee 100644 (file)
@@ -960,28 +960,6 @@ static int condition_test_needs_update(Condition *c, char **env) {
         return timespec_load_nsec(&usr.st_mtim) > timestamp;
 }
 
-static bool in_first_boot(void) {
-        static int first_boot = -1;
-        int r;
-
-        if (first_boot >= 0)
-                return first_boot;
-
-        const char *e = secure_getenv("SYSTEMD_FIRST_BOOT");
-        if (e) {
-                r = parse_boolean(e);
-                if (r < 0)
-                        log_debug_errno(r, "Failed to parse $SYSTEMD_FIRST_BOOT, ignoring: %m");
-                else
-                        return (first_boot = r);
-        }
-
-        r = RET_NERRNO(access("/run/systemd/first-boot", F_OK));
-        if (r < 0 && r != -ENOENT)
-                log_debug_errno(r, "Failed to check if /run/systemd/first-boot exists, assuming no: %m");
-        return r >= 0;
-}
-
 static int condition_test_first_boot(Condition *c, char **env) {
         int r;
 
index eaee54ee57abc288844cdf18af53b5c6b3318df0..a11737148251835ed86eb7dce519cf2dba268249 100644 (file)
@@ -24,6 +24,7 @@
 #include "find-esp.h"
 #include "format-util.h"
 #include "fs-util.h"
+#include "initrd-util.h"
 #include "io-util.h"
 #include "json-util.h"
 #include "log.h"
 #include "mkdir.h"
 #include "parse-util.h"
 #include "path-util.h"
+#include "proc-cmdline.h"
 #include "random-util.h"
 #include "recurse-dir.h"
 #include "sparse-endian.h"
 #include "stat-util.h"
+#include "string-table.h"
 #include "string-util.h"
 #include "tmpfile-util.h"
 #include "tpm2-pcr.h"
@@ -366,6 +369,39 @@ int get_credential_user_password(const char *username, char **ret_password, bool
         return r;
 }
 
+static const char* const credential_boot_policy_table[_CRED_BOOT_POLICY_MAX] = {
+        [CRED_BOOT_STRICT]  = "strict",
+        [CRED_BOOT_TOFU]    = "tofu",
+        [CRED_BOOT_RELAXED] = "relaxed",
+        [CRED_BOOT_OFF]     = "off",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(credential_boot_policy, CredentialBootPolicy);
+
+bool credential_boot_policy_accepts_null(CredentialBootPolicy policy, bool first_boot, bool have_tpm2, bool secure_boot) {
+
+        /* Decides whether a null-key encrypted credential (which offers neither confidentiality nor
+         * authenticity) may be accepted, given the configured policy and the current system state. */
+
+        switch (policy) {
+
+        case CRED_BOOT_STRICT:
+                return false;
+
+        case CRED_BOOT_TOFU:
+                return first_boot || !have_tpm2;
+
+        case CRED_BOOT_RELAXED:
+                return !secure_boot || !have_tpm2;
+
+        case CRED_BOOT_OFF:
+                return true;
+
+        default:
+                assert_not_reached();
+        }
+}
+
 #if HAVE_OPENSSL
 
 #define CREDENTIAL_HOST_SECRET_SIZE 4096
@@ -1195,6 +1231,72 @@ int encrypt_credential_and_warn(
         return 0;
 }
 
+static CredentialBootPolicy query_credential_boot_policy(void) {
+        static CredentialBootPolicy cached = _CRED_BOOT_POLICY_INVALID;
+        _cleanup_free_ char *value = NULL;
+        int r;
+
+        if (cached >= 0)
+                return cached;
+
+        /* default to RELAXED if invalid or unset */
+        cached = CRED_BOOT_RELAXED;
+        r = proc_cmdline_get_key("systemd.credentials_boot_policy", PROC_CMDLINE_STRIP_RD_PREFIX, &value);
+        if (r < 0)
+                log_debug_errno(r, "Failed to read systemd.credentials_boot_policy= from kernel command line, ignoring: %m");
+        else if (r > 0) {
+                CredentialBootPolicy p = credential_boot_policy_from_string(value);
+                if (p < 0)
+                        log_warning("Invalid systemd.credentials_boot_policy= value '%s', ignoring.", value);
+                else
+                        cached = p;
+        }
+
+        return cached;
+}
+
+static int check_null_key_policy(CredentialFlags flags) {
+        if (FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL))
+                return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON),
+                                       "Credential uses null key, but that's not allowed, refusing.");
+
+        if (FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL))
+                return 0;
+
+        /* So this is a credential encrypted with a zero length key. We support this to cover for the
+         * case where neither a host key not a TPM2 are available (specifically: initrd environments
+         * where the host key is not yet accessible and no TPM2 chip exists at all), to minimize
+         * different codeflow for TPM2 and non-TPM2 codepaths. Of course, credentials encoded this
+         * way offer no confidentiality nor authenticity. Because of that it's important we refuse to
+         * use them on systems that actually *do* have a TPM2 chip – if we are in SecureBoot
+         * mode. Otherwise an attacker could hand us credentials like this and we'd use them thinking
+         * they are trusted, even though they are not.
+         *
+         * Which conditions actually lead us to accept a null-key credential is configurable via
+         * systemd.credentials_boot_policy=, which also covers the first boot case (before any key
+         * exists yet); the decision itself is made in credential_boot_policy_accepts_null(). */
+
+        CredentialBootPolicy policy = query_credential_boot_policy();
+        bool first_boot = in_first_boot(), have_tpm2 = efi_has_tpm2(), secure_boot = is_efi_secure_boot();
+
+        if (!credential_boot_policy_accepts_null(policy, first_boot, have_tpm2, secure_boot))
+                return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON),
+                                       "Credential uses null key, but systemd.credentials_boot_policy=%s refuses it here (TPM2=%s, SecureBoot=%s, first boot=%s).",
+                                       credential_boot_policy_to_string(policy),
+                                       yes_no(have_tpm2), yes_no(secure_boot), yes_no(first_boot));
+
+        /* Accepting a null-key credential on a tpm2 host is undesired so keep it auditable */
+        if (have_tpm2 && !first_boot)
+                log_warning("Credential uses null key intended for use when TPM2 is absent, but TPM2 is present! "
+                            "Accepting anyway, under systemd.credentials_boot_policy=%s.",
+                            credential_boot_policy_to_string(policy));
+        else
+                log_debug("Credential uses null key, accepted under systemd.credentials_boot_policy=%s.",
+                          credential_boot_policy_to_string(policy));
+
+        return 0;
+}
+
 int decrypt_credential_and_warn(
                 const char *validate_name,
                 usec_t validate_timestamp,
@@ -1254,29 +1356,9 @@ int decrypt_credential_and_warn(
         }
 
         if (sd_id128_equal(h->id, CRED_AES256_GCM_BY_NULL)) {
-                if (FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL))
-                        return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON),
-                                               "Credential uses null key, but that's not allowed, refusing.");
-
-                if (!FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) {
-                        /* So this is a credential encrypted with a zero length key. We support this to cover for the
-                         * case where neither a host key not a TPM2 are available (specifically: initrd environments
-                         * where the host key is not yet accessible and no TPM2 chip exists at all), to minimize
-                         * different codeflow for TPM2 and non-TPM2 codepaths. Of course, credentials encoded this
-                         * way offer no confidentiality nor authenticity. Because of that it's important we refuse to
-                         * use them on systems that actually *do* have a TPM2 chip – if we are in SecureBoot
-                         * mode. Otherwise an attacker could hand us credentials like this and we'd use them thinking
-                         * they are trusted, even though they are not. */
-
-                        if (efi_has_tpm2()) {
-                                if (is_efi_secure_boot())
-                                        return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON),
-                                                               "Credential uses null key intended for fallback use when TPM2 is absent — but TPM2 is present, and SecureBoot is enabled, refusing.");
-
-                                log_warning("Credential uses null key intended for use when TPM2 is absent, but TPM2 is present! Accepting anyway, since SecureBoot is disabled.");
-                        } else
-                                log_debug("Credential uses null key intended for use when TPM2 is absent, and TPM2 indeed is absent. Accepting.");
-                }
+                r = check_null_key_policy(flags);
+                if (r < 0)
+                        return r;
         }
 
         if (CRED_KEY_IS_SCOPED(h->id)) {
index 32c5a0ba88d6349f708bd5623da1ebae1724f1c7..3d058ecf6a204dcdfb7709f4ada03c09e8240866 100644 (file)
@@ -66,6 +66,19 @@ typedef enum CredentialFlags {
         CREDENTIAL_IPC_ALLOW_INTERACTIVE = 1 << 3,
 } CredentialFlags;
 
+typedef enum CredentialBootPolicy {
+        CRED_BOOT_STRICT,   /* never accept a null key */
+        CRED_BOOT_TOFU,     /* accept a null key during first boot or when no TPM2 is available */
+        CRED_BOOT_RELAXED,  /* accept a null key when SecureBoot is off or when no TPM2 is available */
+        CRED_BOOT_OFF,      /* always accept a null key */
+        _CRED_BOOT_POLICY_MAX,
+        _CRED_BOOT_POLICY_INVALID = -EINVAL,
+} CredentialBootPolicy;
+
+DECLARE_STRING_TABLE_LOOKUP(credential_boot_policy, CredentialBootPolicy);
+
+bool credential_boot_policy_accepts_null(CredentialBootPolicy policy, bool first_boot, bool have_tpm2, bool secure_boot);
+
 /* The four modes we support: keyed only by on-disk key, only by TPM2 HMAC key, and by the combination of
  * both, as well as one with a fixed zero length key if TPM2 is missing (the latter of course provides no
  * authenticity or confidentiality, but is still useful for integrity protection, and makes things simpler
index e380550a5a70ce1b819d96020daca8b17b159717..cbcf0c3444cc7ff41421d38a839c112172159e67 100644 (file)
@@ -228,6 +228,35 @@ TEST(credential_encrypt_decrypt) {
                 ASSERT_OK_ERRNO(setenv("SYSTEMD_CREDENTIAL_SECRET", ec, true));
 }
 
+TEST(credential_boot_policy) {
+
+        /* String table round-trip */
+        for (CredentialBootPolicy p = 0; p < _CRED_BOOT_POLICY_MAX; p++) {
+                const char *s = credential_boot_policy_to_string(p);
+                ASSERT_NOT_NULL(s);
+                ASSERT_EQ(credential_boot_policy_from_string(s), p);
+        }
+        ASSERT_STREQ(credential_boot_policy_to_string(CRED_BOOT_TOFU), "tofu");
+        ASSERT_TRUE(credential_boot_policy_from_string("bogus") < 0);
+
+        /* Exhaustively check the accept decision against every (first_boot, have_tpm2, secure_boot) state */
+        for (int first_boot = 0; first_boot < 2; first_boot++)
+                for (int have_tpm2 = 0; have_tpm2 < 2; have_tpm2++)
+                        for (int secure_boot = 0; secure_boot < 2; secure_boot++) {
+                                /* strict never accepts a null key, off always does */
+                                ASSERT_FALSE(credential_boot_policy_accepts_null(CRED_BOOT_STRICT, first_boot, have_tpm2, secure_boot));
+                                ASSERT_TRUE(credential_boot_policy_accepts_null(CRED_BOOT_OFF, first_boot, have_tpm2, secure_boot));
+
+                                /* tofu accepts when first boot, or no TPM2 */
+                                ASSERT_EQ(credential_boot_policy_accepts_null(CRED_BOOT_TOFU, first_boot, have_tpm2, secure_boot),
+                                          first_boot || !have_tpm2);
+
+                                /* relaxed accepts when SecureBoot off, or no TPM2 */
+                                ASSERT_EQ(credential_boot_policy_accepts_null(CRED_BOOT_RELAXED, first_boot, have_tpm2, secure_boot),
+                                          !secure_boot || !have_tpm2);
+                        }
+}
+
 TEST(mime_type_matches) {
 
         static const sd_id128_t tags[] = {
index f2321481f50e3b5a2327d55ad0dec6e19a5f34f7..7c2774fa103296550ef6e90e146039bb6669a228 100755 (executable)
@@ -168,6 +168,48 @@ systemd-creds decrypt /tmp/cred.enc /tmp/cred.dec
 diff /tmp/cred.orig /tmp/cred.dec
 rm -f /tmp/cred.{enc,dec}
 
+# Null-key credentials and the systemd.credentials_boot_policy= setting.
+#
+# A null-key credential ("--with-key=null") is wrapped in the normal systemd-creds
+# envelope but offers neither confidentiality nor authenticity, so whether it is
+# accepted at decrypt time is governed by systemd.credentials_boot_policy= and the system
+# state. We only assert the branches that are deterministic regardless of the test
+# bed's TPM2/SecureBoot state (which is fixed and has no test override here); the
+# full accepts_null() truth table for all states is covered by test-creds.c.
+systemd-creds --name=nullcred --with-key=null encrypt /tmp/cred.orig /tmp/cred.enc
+
+# --allow-null bypasses the policy, even under the strictest setting.
+SYSTEMD_PROC_CMDLINE="systemd.credentials_boot_policy=strict" \
+    systemd-creds --name=nullcred --allow-null decrypt /tmp/cred.enc /tmp/cred.dec
+diff /tmp/cred.orig /tmp/cred.dec
+rm -f /tmp/cred.dec
+
+# --refuse-null always refuses, even under the most permissive setting.
+(! SYSTEMD_PROC_CMDLINE="systemd.credentials_boot_policy=off" \
+    systemd-creds --name=nullcred --refuse-null decrypt /tmp/cred.enc /tmp/cred.dec)
+
+# strict never accepts a null key, regardless of system state.
+(! SYSTEMD_PROC_CMDLINE="systemd.credentials_boot_policy=strict" \
+    systemd-creds --name=nullcred decrypt /tmp/cred.enc /tmp/cred.dec)
+
+# off always accepts a null key, regardless of system state.
+SYSTEMD_PROC_CMDLINE="systemd.credentials_boot_policy=off" \
+    systemd-creds --name=nullcred decrypt /tmp/cred.enc /tmp/cred.dec
+diff /tmp/cred.orig /tmp/cred.dec
+rm -f /tmp/cred.dec
+
+# An invalid policy value falls back to the default (relaxed), which accepts during first boot.
+SYSTEMD_PROC_CMDLINE="systemd.credentials_boot_policy=bogus" SYSTEMD_FIRST_BOOT=1 \
+    systemd-creds --name=nullcred decrypt /tmp/cred.enc /tmp/cred.dec
+diff /tmp/cred.orig /tmp/cred.dec
+rm -f /tmp/cred.dec
+
+# tofu accepts during first boot, independent of TPM2/SecureBoot.
+SYSTEMD_PROC_CMDLINE="systemd.credentials_boot_policy=tofu" SYSTEMD_FIRST_BOOT=1 \
+    systemd-creds --name=nullcred decrypt /tmp/cred.enc /tmp/cred.dec
+diff /tmp/cred.orig /tmp/cred.dec
+rm -f /tmp/cred.{enc,dec}
+
 (! unshare -m bash -exc "mount -t tmpfs tmpfs /run/credentials && systemd-creds list")
 (! unshare -m bash -exc "mount -t tmpfs tmpfs /run/credentials && systemd-creds --system list")
 (! CREDENTIALS_DIRECTORY="" systemd-creds list)