]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: import trusted initrd credentials
authorPaul Meyer <katexochen0@gmail.com>
Wed, 3 Jun 2026 11:23:27 +0000 (13:23 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Wed, 24 Jun 2026 10:47:42 +0000 (12:47 +0200)
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 <katexochen0@gmail.com>
src/core/import-creds.c

index 488d63eb41d816825c855f2616b499055e980072..98675d2fc52e4b0d42f7ea96c498a3eb5d99c779 100644 (file)
@@ -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();