diff --git a/docs/CREDENTIALS.md b/docs/CREDENTIALS.md index 766d84fdba..bbd92ad3c9 100644 --- a/docs/CREDENTIALS.md +++ b/docs/CREDENTIALS.md @@ -50,10 +50,11 @@ purpose. Specifically, the following features are provided: 6. Service credentials are placed in non-swappable memory. (If permissions allow it, via `ramfs`.) -7. Credentials may be acquired from a hosting VM hypervisor (qemu `fw_cfg`), a - hosting container manager, the kernel command line, or from the UEFI - environment and the EFI System Partition (via `systemd-stub`). Such system - credentials may then be propagated into individual services as needed. +7. Credentials may be acquired from a hosting VM hypervisor (SMBIOS OEM strings + or qemu `fw_cfg`), a hosting container manager, the kernel command line, or + from the UEFI environment and the EFI System Partition (via + `systemd-stub`). Such system credentials may then be propagated into + individual services as needed. 8. Credentials are an effective way to pass parameters into services that run with `RootImage=` or `RootDirectory=` and thus cannot read these resources @@ -254,10 +255,18 @@ services where they are ultimately consumed. the [Container Interface](CONTAINER_INTERFACE.md) documentation. -2. Quite similar, qemu VMs can be invoked with `-fw_cfg +2. Quite similar, VMs can be passed credentials via SMBIOS OEM strings (example + qemu command line switch `-smbios + type=11,value=io.systemd.credential:foo=bar` or `-smbios + type=11,value=io.systemd.credential.binary:foo=YmFyCg==`, the latter taking + a Base64 encoded argument to permit binary credentials being passed + in). Alternatively, qemu VMs can be invoked with `-fw_cfg name=opt/io.systemd.credentials/foo,string=bar` to pass credentials from - host through the hypervisor into the VM. (This specific switch would set - credential `foo` to `bar`.) + host through the hypervisor into the VM via qemu's `fw_cfg` mechanism. (All + three of these specific switches would set credential `foo` to `bar`.) + Passing credentials via the SMBIOS mechanism is typically preferable over + `fw_cfg` since it is faster and less specific to the chosen VMM + implementation. 3. Credentials can also be passed into a system via the kernel command line, via the `systemd.set-credential=` kernel command line option. Note though @@ -297,7 +306,7 @@ qemu-system-x86_64 \ -drive if=none,id=hd,file=test.raw,format=raw \ -device virtio-scsi-pci,id=scsi \ -device scsi-hd,drive=hd,bootindex=1 \ - -fw_cfg name=opt/io.systemd.credentials/mycred,string=supersecret + -smbios type=11,value=io.systemd.credential:mycred=supersecret ``` Either of these lines will boot a disk image `test.raw`, once as container via @@ -363,8 +372,8 @@ qemu-system-x86_64 \ -drive if=none,id=hd,file=test.raw,format=raw \ -device virtio-scsi-pci,id=scsi \ -device scsi-hd,drive=hd,bootindex=1 \ - -fw_cfg name=opt/io.systemd.credentials/passwd.hashed-password.root,string=$(mkpasswd mysecret) \ - -fw_cfg name=opt/io.systemd.credentials/firstboot.locale,string=C.UTF-8 + -smbios type=11,value=io.systemd.credential:passwd.hashed-password.root=$(mkpasswd mysecret) \ + -smbios type=11,value=io.systemd.credential:firstboot.locale=C.UTF-8 ``` ## Relevant Paths diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 3d7ec1e202..055858ef04 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -3125,12 +3125,20 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX The service manager itself may receive system credentials that can be propagated to services from a hosting container manager or VM hypervisor. See the Container Interface documentation for details - about the former. For the latter, use the qemu fw_cfg node + about the former. For the latter, pass DMI/SMBIOS OEM string table entries (field type + 11) with a prefix of io.systemd.credential: or + io.systemd.credential.binary:. In both cases a key/value pair separated by + = is expected, in the latter case the right-hand side is Base64 decoded when + parsed (thus permitting binary data to be passed in). Example qemu switch: -smbios + type=11,value=io.systemd.credential:xx=yy, or -smbios + type=11,value=io.systemd.credential.binary:rick=TmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXA=. Alternatively, + use the qemu fw_cfg node opt/io.systemd.credentials/. Example qemu switch: -fw_cfg name=opt/io.systemd.credentials/mycred,string=supersecret. They may also be specified on the kernel command line using the systemd.set_credential= switch (see - systemd1) - and from the UEFI firmware environment via + systemd1) and from + the UEFI firmware environment via systemd-stub7. If referencing an AF_UNIX stream socket to connect to, the connection will diff --git a/man/systemd.xml b/man/systemd.xml index e526a1caea..30484e09a9 100644 --- a/man/systemd.xml +++ b/man/systemd.xml @@ -965,7 +965,8 @@ systemd.import_credentials= Takes a boolean argument. If false disables importing credentials from the kernel - command line, qemu_fw_cfg subsystem or the kernel command line. + command line, the DMI/SMBIOS OEM string table, the qemu_fw_cfg subsystem or the EFI kernel + stub. diff --git a/src/core/import-creds.c b/src/core/import-creds.c index 53796484ee..3324a6eaab 100644 --- a/src/core/import-creds.c +++ b/src/core/import-creds.c @@ -4,9 +4,11 @@ #include "copy.h" #include "creds-util.h" +#include "escape.h" #include "fileio.h" #include "format-util.h" #include "fs-util.h" +#include "hexdecoct.h" #include "import-creds.h" #include "io-util.h" #include "mkdir-label.h" @@ -24,7 +26,7 @@ * generators invoked by it) can acquire credentials from outside, to mimic how we support it for containers, * but on VM/physical environments. * - * This does three things: + * This does four things: * * 1. It imports credentials picked up by sd-boot (and placed in the /.extra/credentials/ dir in the initrd) * and puts them in /run/credentials/@encrypted/. Note that during the initrd→host transition the initrd root @@ -41,6 +43,11 @@ * /sys/firmware/qemu_fw_cfg/by_name/opt/io.systemd.credentials/ is picked up and also placed in * /run/credentials/@system/. * + * 4. It imports credentials passed in via the DMI/SMBIOS OEM string tables, quite similar to fw_cfg. It + * looks for strings starting with "io.systemd.credential:" and "io.systemd.credential.binary:". Both + * expect a key=value assignment, but in the latter case the value is Base64 decoded, allowing binary + * credentials to be passed in. + * * If it picked up any credentials it will set the $CREDENTIALS_DIRECTORY and * $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables to point to these directories, so that processes * can find them there later on. If "ramfs" is available $CREDENTIALS_DIRECTORY will be backed by it (but @@ -446,26 +453,177 @@ static int import_credentials_qemu(ImportCredentialContext *c) { return 0; } +static int parse_smbios_strings(ImportCredentialContext *c, const char *data, size_t size) { + size_t left, skip; + const char *p; + int r; + + assert(c); + assert(data || size == 0); + + /* Unpacks a packed series of SMBIOS OEM vendor strings. These are a series of NUL terminated + * strings, one after the other. */ + + for (p = data, left = size; left > 0; p += skip, left -= skip) { + _cleanup_free_ void *buf = NULL; + _cleanup_free_ char *cn = NULL; + _cleanup_close_ int nfd = -1; + const char *nul, *n, *eq; + const void *cdata; + size_t buflen, cdata_len; + bool unbase64; + + nul = memchr(p, 0, left); + if (nul) + skip = (nul - p) + 1; + else { + nul = p + left; + skip = left; + } + + if (nul - p == 0) /* Skip empty strings */ + continue; + + /* Only care about strings starting with either of these two prefixes */ + if ((n = memory_startswith(p, nul - p, "io.systemd.credential:"))) + unbase64 = false; + else if ((n = memory_startswith(p, nul - p, "io.systemd.credential.binary:"))) + unbase64 = true; + else { + _cleanup_free_ char *escaped = NULL; + + escaped = cescape_length(p, nul - p); + log_debug("Ignoring OEM string: %s", strnull(escaped)); + continue; + } + + eq = memchr(n, '=', nul - n); + if (!eq) { + log_warning("SMBIOS OEM string lacks '=' character, ignoring."); + continue; + } + + cn = memdup_suffix0(n, eq - n); + if (!cn) + return log_oom(); + + if (!credential_name_valid(cn)) { + log_warning("SMBIOS credential name '%s' is not valid, ignoring: %m", cn); + continue; + } + + /* Optionally base64 decode the data, if requested, to allow binary credentials */ + if (unbase64) { + r = unbase64mem(eq + 1, nul - (eq + 1), &buf, &buflen); + if (r < 0) { + log_warning_errno(r, "Failed to base64 decode credential '%s', ignoring: %m", cn); + continue; + } + + cdata = buf; + cdata_len = buflen; + } else { + cdata = eq + 1; + cdata_len = nul - (eq + 1); + } + + if (!credential_size_ok(c, cn, cdata_len)) + continue; + + r = acquire_credential_directory(c); + if (r < 0) + return r; + + nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, cn); + if (nfd == -EEXIST) + continue; + if (nfd < 0) + return nfd; + + r = loop_write(nfd, cdata, cdata_len, /* do_poll= */ false); + if (r < 0) { + (void) unlinkat(c->target_dir_fd, cn, 0); + return log_error_errno(r, "Failed to write credential: %m"); + } + + c->size_sum += cdata_len; + c->n_credentials++; + + log_debug("Successfully processed SMBIOS credential '%s'.", cn); + } + + return 0; +} + +static int import_credentials_smbios(ImportCredentialContext *c) { + int r; + + /* Parses DMI OEM strings fields (SMBIOS type 11), as settable with qemu's -smbios type=11,value=… switch. */ + + for (unsigned i = 0;; i++) { + struct dmi_field_header { + uint8_t type; + uint8_t length; + uint16_t handle; + uint8_t count; + char contents[]; + } _packed_ *dmi_field_header; + _cleanup_free_ char *p = NULL; + _cleanup_free_ void *data = NULL; + size_t size; + + assert_cc(offsetof(struct dmi_field_header, contents) == 5); + + if (asprintf(&p, "/sys/firmware/dmi/entries/11-%u/raw", i) < 0) + return log_oom(); + + r = read_virtual_file(p, sizeof(dmi_field_header) + CREDENTIALS_TOTAL_SIZE_MAX, (char**) &data, &size); + if (r < 0) { + /* Once we reach ENOENT there are no more DMI Type 11 fields around. */ + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, "Failed to open '%p', ignoring: %m", p); + break; + } + + if (size < offsetof(struct dmi_field_header, contents)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "DMI field header of '%p' too short.", p); + + dmi_field_header = data; + if (dmi_field_header->type != 11 || + dmi_field_header->length != offsetof(struct dmi_field_header, contents)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Invalid DMI field header."); + + r = parse_smbios_strings(c, dmi_field_header->contents, size - offsetof(struct dmi_field_header, contents)); + if (r < 0) + return r; + + if (i == UINT_MAX) /* Prevent overflow */ + break; + } + + return 0; +} + static int import_credentials_trusted(void) { _cleanup_(import_credentials_context_free) ImportCredentialContext c = { .target_dir_fd = -1, }; - int q, r; + int q, w, r; r = import_credentials_qemu(&c); + w = import_credentials_smbios(&c); q = import_credentials_proc_cmdline(&c); if (c.n_credentials > 0) { int z; - log_debug("Imported %u credentials from kernel command line/fw_cfg.", c.n_credentials); + log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg.", c.n_credentials); z = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY"); if (z < 0) return z; } - return r < 0 ? r : q; + return r < 0 ? r : w < 0 ? w : q; } static int symlink_credential_dir(const char *envvar, const char *path, const char *where) { diff --git a/test/TEST-54-CREDS/test.sh b/test/TEST-54-CREDS/test.sh index 8d5d796cc8..7bbc94ebc0 100755 --- a/test/TEST-54-CREDS/test.sh +++ b/test/TEST-54-CREDS/test.sh @@ -4,7 +4,7 @@ set -e TEST_DESCRIPTION="test credentials" NSPAWN_ARGUMENTS="${NSPAWN_ARGUMENTS:-} --set-credential=mynspawncredential:strangevalue" -QEMU_OPTIONS="${QEMU_OPTIONS:-} -fw_cfg name=opt/io.systemd.credentials/myqemucredential,string=othervalue" +QEMU_OPTIONS="${QEMU_OPTIONS:-} -fw_cfg name=opt/io.systemd.credentials/myqemucredential,string=othervalue -smbios type=11,value=io.systemd.credential:smbioscredential=magicdata -smbios type=11,value=io.systemd.credential.binary:binarysmbioscredential=bWFnaWNiaW5hcnlkYXRh" KERNEL_APPEND="${KERNEL_APPEND:-} systemd.set_credential=kernelcmdlinecred:uff systemd.set_credential=sysctl.extra:kernel.domainname=sysctltest rd.systemd.import_credentials=no" # shellcheck source=test/test-functions diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh index 06f3beb287..151a7987d5 100755 --- a/test/units/testsuite-54.sh +++ b/test/units/testsuite-54.sh @@ -29,6 +29,10 @@ elif [ -d /sys/firmware/qemu_fw_cfg/by_name ]; then # Verify that passing creds through kernel cmdline works [ "$(systemd-creds --system cat kernelcmdlinecred)" = "uff" ] + # And that it also works via SMBIOS + [ "$(systemd-creds --system cat smbioscredential)" = "magicdata" ] + [ "$(systemd-creds --system cat binarysmbioscredential)" = "magicbinarydata" ] + # If we aren't run in nspawn, we are run in qemu systemd-detect-virt -q -v expected_credential=myqemucredential