credentials: add policy that can allow key=null creds from the ESP

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.
This commit is contained in:
Michael Vogt
2026-07-09 16:48:26 +02:00
parent add910a838
commit cb7f95ff5f
10 changed files with 278 additions and 45 deletions
+24
View 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;
}
+2
View File
@@ -5,3 +5,5 @@
bool in_initrd(void);
void in_initrd_force(bool value);
bool in_first_boot(void);
-22
View 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;
+105 -23
View 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"
@@ -31,10 +32,12 @@
#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)) {
+13
View 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
+29
View 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[] = {