…
```
+### 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,
<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>
<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>
/* 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;
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;
+}
bool in_initrd(void);
void in_initrd_force(bool value);
+
+bool in_first_boot(void);
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;
#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"
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
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,
}
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)) {
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
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[] = {
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)