diff --git a/docs/CREDENTIALS.md b/docs/CREDENTIALS.md index 149cf2f7db2..5260db6b332 100644 --- a/docs/CREDENTIALS.md +++ b/docs/CREDENTIALS.md @@ -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, diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 768c7653fb4..9134c987c9a 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -67,6 +67,7 @@ systemd.set_credential= systemd.set_credential_binary= systemd.import_credentials= + systemd.credentials_boot_policy= systemd.reload_limit_interval_sec= systemd.reload_limit_burst= systemd.minimum_uptime_sec= diff --git a/man/systemd.xml b/man/systemd.xml index 557cd766482..51ce1854fbb 100644 --- a/man/systemd.xml +++ b/man/systemd.xml @@ -965,6 +965,41 @@ + + systemd.credentials_boot_policy= + + Controls under which conditions a credential encrypted with a null + key (i.e. a credential wrapped in a systemd-creds envelope but offering neither + confidentiality nor authenticity, see + systemd-creds1) + 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 , , or + : + + + never accepts a null key, i.e. always + insists on proper encryption. + + (trust on first use) accepts a null key + during first boot, or when no TPM2 device is available. + + accepts a null key when SecureBoot is + disabled, or when no TPM2 device is available. + + always accepts a null key. + + + Defaults to . 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. + + For further information see System and Service + Credentials documentation. + + + + quiet diff --git a/src/basic/initrd-util.c b/src/basic/initrd-util.c index c45920b3070..6c273c2e8a4 100644 --- a/src/basic/initrd-util.c +++ b/src/basic/initrd-util.c @@ -38,3 +38,27 @@ bool in_initrd(void) { void in_initrd_force(bool value) { saved_in_initrd = value; } + +int in_first_boot(void) { + static int first_boot_env_parse_cached = -1; + int r; + + if (first_boot_env_parse_cached >= 0) + return first_boot_env_parse_cached; + + r = secure_getenv_bool("SYSTEMD_FIRST_BOOT"); + if (r >= 0) + return (first_boot_env_parse_cached = r); + if (r != -ENXIO) + log_debug_errno(r, "Failed to parse $SYSTEMD_FIRST_BOOT, ignoring: %m"); + + /* This is not cached and must *never* be cached, other parts of systemd write it to signal + * an update to the first-boot state. */ + r = RET_NERRNO(access("/run/systemd/first-boot", F_OK)); + if (r >= 0) + return true; + if (r == -ENOENT) + return false; + + return log_debug_errno(r, "Failed to check /run/systemd/first-boot: %m"); +} diff --git a/src/basic/initrd-util.h b/src/basic/initrd-util.h index 387c2c6717c..f83b5c9d5d0 100644 --- a/src/basic/initrd-util.h +++ b/src/basic/initrd-util.h @@ -5,3 +5,5 @@ bool in_initrd(void); void in_initrd_force(bool value); + +int in_first_boot(void); diff --git a/src/core/main.c b/src/core/main.c index 84e6c75bef7..2b4036381cc 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2615,6 +2615,11 @@ static void log_execution_mode(bool *ret_first_boot) { } } + /* Make the first-boot file visible now: the credential import + * consults in_first_boot() for systemd.credentials_boot_policy= which runs + * before the manager object is created. */ + (void) update_first_boot_file(first_boot); + assert_se(uname(&uts) >= 0); if (strverscmp_improved(uts.release, KERNEL_BASELINE_VERSION) < 0) diff --git a/src/core/manager.c b/src/core/manager.c index 27ca84c4e9e..f5daa59e1ab 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -4842,21 +4842,32 @@ fail: } void manager_set_first_boot(Manager *m, bool b) { + int r; + assert(m); if (!MANAGER_IS_SYSTEM(m)) return; if (m->first_boot != (int) b) { - if (b) - (void) touch("/run/systemd/first-boot"); - else - (void) unlink("/run/systemd/first-boot"); + r = update_first_boot_file(b); + if (r < 0) + log_warning_errno(r, "Failed to update the first-boot file, ignoring: %m"); } m->first_boot = b; } +int update_first_boot_file(bool b) { + if (b) + return touch("/run/systemd/first-boot"); + + if (unlink("/run/systemd/first-boot") < 0 && errno != ENOENT) + return -errno; + + return 0; +} + void manager_disable_confirm_spawn(void) { (void) touch("/run/systemd/confirm_spawn_disabled"); } diff --git a/src/core/manager.h b/src/core/manager.h index 8262bfbec85..12d6cf44709 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -719,6 +719,8 @@ void manager_log_caller(Manager *manager, PidRef *caller, const char *method); int manager_allocate_idle_pipe(Manager *m); +int update_first_boot_file(bool b); + void unit_defaults_init(UnitDefaults *defaults, RuntimeScope scope); void unit_defaults_done(UnitDefaults *defaults); diff --git a/src/random-seed/random-seed-tool.c b/src/random-seed/random-seed-tool.c index 289291761f0..9f9439c8c57 100644 --- a/src/random-seed/random-seed-tool.c +++ b/src/random-seed/random-seed-tool.c @@ -15,6 +15,7 @@ #include "fd-util.h" #include "format-table.h" #include "fs-util.h" +#include "initrd-util.h" #include "io-util.h" #include "log.h" #include "main-func.h" @@ -88,8 +89,8 @@ static CreditEntropy may_credit(int seed_fd) { /* Don't credit the random seed if we are in first-boot mode, because we are supposed to start from * scratch. This is a safety precaution for cases where people ship "golden" images with empty * /etc but populated /var that contains a random seed. */ - r = RET_NERRNO(access("/run/systemd/first-boot", F_OK)); - if (r == -ENOENT) + r = in_first_boot(); + if (r == 0) /* All is good, we are not in first-boot mode. */ return CREDIT_ENTROPY_YES_PLEASE; if (r < 0) { diff --git a/src/shared/condition.c b/src/shared/condition.c index 2a831371c6c..cc13f7bd0b6 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -966,28 +966,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; @@ -1002,7 +980,7 @@ static int condition_test_first_boot(Condition *c, char **env) { if (r < 0) return r; - return in_first_boot() == r; + return (in_first_boot() > 0) == r; } static int condition_test_environment(Condition *c, char **env) { diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 54a8a9abead..b1b4c8a34d9 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -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,75 @@ 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 have_tpm2 = efi_has_tpm2(), secure_boot = is_efi_secure_boot(); + /* in_first_boot() can return <0 on error: use the safe default in this case (not-first-boot). */ + bool first_boot = in_first_boot() > 0; + + 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 +1359,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)) { diff --git a/src/shared/creds-util.h b/src/shared/creds-util.h index 32c5a0ba88d..3d058ecf6a2 100644 --- a/src/shared/creds-util.h +++ b/src/shared/creds-util.h @@ -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 diff --git a/src/test/test-creds.c b/src/test/test-creds.c index e380550a5a7..cbcf0c3444c 100644 --- a/src/test/test-creds.c +++ b/src/test/test-creds.c @@ -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[] = { diff --git a/test/units/TEST-54-CREDS.sh b/test/units/TEST-54-CREDS.sh index f2321481f50..7c2774fa103 100755 --- a/test/units/TEST-54-CREDS.sh +++ b/test/units/TEST-54-CREDS.sh @@ -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)