From: Paul Meyer Date: Wed, 3 Jun 2026 11:23:27 +0000 (+0200) Subject: core: import trusted initrd credentials X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9e7b8b734f16e49d9bbadd5f99452293d0cd9b48;p=thirdparty%2Fsystemd.git core: import trusted initrd credentials PID1's import_credentials_boot() so far always treated initrd-delivered credentials as untrusted: anything found under /.extra/credentials/ was routed into ENCRYPTED_CREDENTIALS_DIRECTORY, forcing consumers to use LoadCredentialEncrypted= and provide credentials in systemd-creds encrypted form. That matches the trust model for stub which sources credentials from the EFI System Partition (mountable and editable offline). Host-side producers that take responsibility for the trust of the cpio they hand to the kernel have a different model: e.g. systemd-vmspawn builds an initrd-credentials cpio whose bytes are covered by the SEV-SNP launch measurement via QEMU's kernel-hashes=on (or, in non-confidential setups, by the host itself being the trust root). Forcing those through the @encrypted bucket would require null-key wrapping on the host and LoadCredentialEncrypted= on the consumer side, a needless API split for unit files that should otherwise be portable between confidential and non-confidential boots. Extend import_credentials_boot() to import credentials from /.extra/system_credentials/, routed into SYSTEM_CREDENTIALS_DIRECTORY. Consumers access these via LoadCredential= directly, with no encrypted-credential ceremony. The per-directory walk is hoisted into a small static helper so the new target bucket can share the existing copy/validation logic; behavior for the existing untrusted paths is unchanged. Also set the new RECURSE_DIR_MUST_BE_REGULAR flag as a drive-by. Signed-off-by: Paul Meyer --- diff --git a/src/core/import-creds.c b/src/core/import-creds.c index 488d63eb41d..98675d2fc52 100644 --- a/src/core/import-creds.c +++ b/src/core/import-creds.c @@ -168,119 +168,158 @@ static int finalize_credentials_dir(const char *dir, const char *envvar) { return 0; } -static int import_credentials_boot(void) { - _cleanup_(import_credentials_context_done) ImportCredentialsContext context = { - .target_dir_fd = -EBADF, - }; +static int import_credentials_from_initrd_path( + ImportCredentialsContext *c, + const char *source_path, + const char *target_dir, + bool with_mount) { + + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_close_ int source_dir_fd = -EBADF; int r; - /* systemd-stub will wrap sidecar *.cred files from the UEFI kernel image directory into initrd - * cpios, so that they unpack into /.extra/. We'll pick them up from there and copy them into /run/ - * so that we can access them during the entire runtime (note that the initrd file system is erased - * during the initrd → host transition). Note that these credentials originate from an untrusted - * source (i.e. the ESP typically) and thus need to be authenticated later. We thus put them in a - * directory separate from the usual credentials which are from a trusted source. */ + source_dir_fd = open(source_path, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (source_dir_fd < 0) { + if (errno == ENOENT) { + log_debug("No credentials passed via %s.", source_path); + return 0; + } - if (!in_initrd()) + log_warning_errno(errno, "Failed to open '%s', ignoring: %m", source_path); return 0; + } - FOREACH_STRING(p, - "/.extra/credentials/", /* specific to this boot menu */ - "/.extra/global_credentials/") { /* boot partition wide */ + r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &de); + if (r < 0) { + log_warning_errno(r, "Failed to read '%s' contents, ignoring: %m", source_path); + return 0; + } - _cleanup_free_ DirectoryEntries *de = NULL; - _cleanup_close_ int source_dir_fd = -EBADF; + FOREACH_ARRAY(i, de->entries, de->n_entries) { + const struct dirent *d = *i; + _cleanup_close_ int cfd = -EBADF, nfd = -EBADF; + _cleanup_free_ char *n = NULL; + const char *e; + struct stat st; - source_dir_fd = open(p, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); - if (source_dir_fd < 0) { - if (errno == ENOENT) { - log_debug("No credentials passed via %s.", p); - continue; - } + e = endswith(d->d_name, ".cred"); + if (!e) + continue; - log_warning_errno(errno, "Failed to open '%s', ignoring: %m", p); + /* drop .cred suffix (which we want in the ESP sidecar dir, but not for our internal + * processing) */ + n = strndup(d->d_name, e - d->d_name); + if (!n) + return log_oom(); + + if (!credential_name_valid(n)) { + log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); continue; } - r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); - if (r < 0) { - log_warning_errno(r, "Failed to read '%s' contents, ignoring: %m", p); + cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC); + if (cfd < 0) { + log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name); continue; } - FOREACH_ARRAY(i, de->entries, de->n_entries) { - const struct dirent *d = *i; - _cleanup_close_ int cfd = -EBADF, nfd = -EBADF; - _cleanup_free_ char *n = NULL; - const char *e; - struct stat st; + if (fstat(cfd, &st) < 0) { + log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name); + continue; + } - e = endswith(d->d_name, ".cred"); - if (!e) - continue; + r = stat_verify_regular(&st); + if (r < 0) { + log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name); + continue; + } - /* drop .cred suffix (which we want in the ESP sidecar dir, but not for our internal - * processing) */ - n = strndup(d->d_name, e - d->d_name); - if (!n) - return log_oom(); + if (!credential_size_ok(c, n, st.st_size)) + continue; - if (!credential_name_valid(n)) { - log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); - continue; - } + r = acquire_credential_directory(c, target_dir, with_mount); + if (r < 0) + return r; - cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC); - if (cfd < 0) { - log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name); - continue; - } + nfd = open_credential_file_for_write(c->target_dir_fd, target_dir, n); + if (nfd == -EEXIST) + continue; + if (nfd < 0) + return nfd; - if (fstat(cfd, &st) < 0) { - log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name); - continue; - } + r = copy_bytes(cfd, nfd, st.st_size, 0); + if (r < 0) { + (void) unlinkat(c->target_dir_fd, n, 0); + return log_error_errno(r, "Failed to create credential '%s': %m", n); + } - r = stat_verify_regular(&st); - if (r < 0) { - log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name); - continue; - } + c->size_sum += st.st_size; + c->n_credentials++; - if (!credential_size_ok(&context, n, st.st_size)) - continue; + log_debug("Successfully copied boot credential '%s'.", n); + } - r = acquire_credential_directory(&context, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ false); - if (r < 0) - return r; + return 0; +} - nfd = open_credential_file_for_write(context.target_dir_fd, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, n); - if (nfd == -EEXIST) - continue; - if (nfd < 0) - return nfd; +static int import_credentials_boot(ImportCredentialsContext *system_ctx) { + _cleanup_(import_credentials_context_done) ImportCredentialsContext encrypted_ctx = { + .target_dir_fd = -EBADF, + }; + unsigned n_system_before; + int r; - r = copy_bytes(cfd, nfd, st.st_size, 0); - if (r < 0) { - (void) unlinkat(context.target_dir_fd, n, 0); - return log_error_errno(r, "Failed to create credential '%s': %m", n); - } + assert(system_ctx); + + /* The initrd may contain two flavours of credentials placed under /.extra/, copied + * across the initrd → host transition before the initrd tmpfs is erased: + * + * - /.extra/credentials/ and /.extra/global_credentials/ — placed by systemd-stub + * from the EFI System Partition. Trust model: untrusted, because the ESP can + * be mounted and edited offline. They are routed into the @encrypted bucket + * where consumers must authenticate them before use (via LoadCredentialEncrypted=). + * + * - /.extra/system_credentials/ — placed by host-side producers that take responsibility + * for the trust (e.g. systemd-vmspawn when its cpio is covered by the SEV-SNP launch + * measurement or when the host is the trust root in non-confidential setups). Routed + * into the @system bucket where consumers can use them directly via LoadCredential=. */ - context.size_sum += st.st_size; - context.n_credentials++; + if (!in_initrd()) + return 0; - log_debug("Successfully copied boot credential '%s'.", n); - } + FOREACH_STRING(p, + "/.extra/credentials/", /* specific to this boot menu */ + "/.extra/global_credentials/") { /* boot partition wide */ + r = import_credentials_from_initrd_path( + &encrypted_ctx, p, + ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, + /* with_mount= */ false); + if (r < 0) + return r; } - if (context.n_credentials > 0) { - log_debug("Imported %u credentials from boot loader.", context.n_credentials); + n_system_before = system_ctx->n_credentials; + + r = import_credentials_from_initrd_path( + system_ctx, "/.extra/system_credentials/", + SYSTEM_CREDENTIALS_DIRECTORY, + /* with_mount= */ true); + if (r < 0) + return r; + + if (encrypted_ctx.n_credentials > 0) { + log_debug("Imported %u encrypted credentials from boot loader.", encrypted_ctx.n_credentials); r = finalize_credentials_dir(ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, "ENCRYPTED_CREDENTIALS_DIRECTORY"); if (r < 0) return r; } + if (system_ctx->n_credentials > n_system_before) + log_debug("Imported %u trusted credentials from boot loader.", system_ctx->n_credentials - n_system_before); + + /* The @system credentials directory is shared with import_credentials_trusted(); the caller finalizes. */ + return 0; } @@ -701,28 +740,27 @@ static int import_credentials_initrd(ImportCredentialsContext *c) { return 0; } -static int import_credentials_trusted(void) { - _cleanup_(import_credentials_context_done) ImportCredentialsContext c = { - .target_dir_fd = -EBADF, - }; - int r, ret = 0; +static int import_credentials_trusted(ImportCredentialsContext *c) { + unsigned n_before; + int ret = 0; + + assert(c); /* This is invoked during early boot when no credentials have been imported so far. (Specifically, if * the $CREDENTIALS_DIRECTORY or $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables are not set * yet.) */ - RET_GATHER(ret, import_credentials_qemu(&c)); - RET_GATHER(ret, import_credentials_smbios(&c)); - RET_GATHER(ret, import_credentials_proc_cmdline(&c)); - RET_GATHER(ret, import_credentials_initrd(&c)); + n_before = c->n_credentials; - if (c.n_credentials > 0) { - log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg/initrd.", c.n_credentials); + RET_GATHER(ret, import_credentials_qemu(c)); + RET_GATHER(ret, import_credentials_smbios(c)); + RET_GATHER(ret, import_credentials_proc_cmdline(c)); + RET_GATHER(ret, import_credentials_initrd(c)); - r = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY"); - if (r < 0) - return r; - } + if (c->n_credentials > n_before) + log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg/initrd.", c->n_credentials - n_before); + + /* The @system credentials directory is shared with import_credentials_boot(); the caller finalizes. */ return ret; } @@ -884,6 +922,9 @@ int import_credentials(void) { RET_GATHER(r, merge_credentials_trusted(received_creds_dir)); } else { + _cleanup_(import_credentials_context_done) ImportCredentialsContext system_ctx = { + .target_dir_fd = -EBADF, + }; bool import; r = proc_cmdline_get_bool("systemd.import_credentials", PROC_CMDLINE_STRIP_RD_PREFIX|PROC_CMDLINE_TRUE_WHEN_MISSING, &import); @@ -894,8 +935,12 @@ int import_credentials(void) { return 0; } - r = import_credentials_boot(); - RET_GATHER(r, import_credentials_trusted()); + /* System credential context is shared so a single CREDENTIALS_TOTAL_SIZE_MAX is enforced. */ + r = import_credentials_boot(&system_ctx); + RET_GATHER(r, import_credentials_trusted(&system_ctx)); + + if (system_ctx.n_credentials > 0) + RET_GATHER(r, finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY")); } report_credentials();