dissect,nspawn,sysext: enable TPM2 unlocking of DDIs, document it

Set DISSECT_IMAGE_TPM2_UNLOCK where unlocking an image's LUKS partitions
via the local TPM2 is wanted and the caller is trusted:

 * systemd-dissect, an admin tool operating on an image the invoking user
   can read anyway;
 * systemd-nspawn --image=, which already prompts for a passphrase
   interactivly, so TPM2 first and passphrase second is the natural
   order;
 * systemd-sysext/systemd-confext, which decrypt without any passphrase,
   so extension images sealed to the host can be activated without user
   interaction.

For sysext/confext setting the flag at merge time is not enough: before
merging, the extension-release metadata is read via image_read_metadata()
in discover-image.c, which uses its own flag set and would already fail
with -ENOKEY on an encrypted image. Give it an extra_flags parameter and
pass the flag from there, too; the other callers (machined, portabled)
pass 0 and keep their behaviour.

Two units need adjusting for this to work when started by the service
manager: systemd-nspawn@.service has DevicePolicy=closed, so allow
/dev/tpmrm0 there, and the sysext/confext services are not ordered
against the TPM2 device showing up, so order them after tpm2.target
like the pcr* services. The initrd variants cannot be ordered the same
way: they run before local-fs-pre.target, while the software TPM fallback
only becomes available after the early ESP mount, which is ordered after
local-fs-pre.target, so a static After=tpm2.target would be an ordering
cycle with systemd.tpm2_software_fallback=1. Hence systemd-tpm2-generator
adds the ordering as a drop-in instead, unless the fallback is in use.
That drop-in also orders them after systemd-pcrphase-initrd.service:
nothing else guarantees the "enter-initrd" phase has been measured into
PCR 11 before the initrd extension units merge their images (the barrier's
Before=systemd-sysext.service is a leftover from when that unit was also
used in the initrd), so tokens bound to a signed PCR 11 policy or a
pcrlock policy would race the measurement.

Deliberately not enabled for now: RootImage=/ExtensionImages=/MountImages=
in units, portabled and gpt-auto. For those an unconditional default is
probably not wanted but a per-unit or per-image switch, which is left for
a follow-up. systemd-mountfsd, which unprivileged systemd-dissect and
systemd-nspawn delegate to, keeps its own flag set and does not honour
the flag, hence TPM2 unlocking is only available when running privileged;
the man pages say so.

Document the behaviour and $SYSTEMD_DISSECT_TPM2_UNLOCK= in the dissect,
nspawn and sysext man pages and in docs/ENVIRONMENT.md.
This commit is contained in:
Adrian Reber committed 2026-10-13 11:05:48 +00:00
1 parent 972ceb2258
commit 600fcc6dcb
22 files changed
+111 -16

No files matched your search

+8
View File
@@ -146,6 +146,14 @@ All tools:
RootImage=/ExtensionImages=/MountImages= in unit files default to enabled,
while other uses default to disabled for this.
* `$SYSTEMD_DISSECT_TPM2_UNLOCK=` — takes a boolean. If false, turns off
unlocking of LUKS encrypted partitions in disk images via a TPM2 token
enrolled in the LUKS2 header (e.g. via `systemd-cryptenroll --tpm2-device=…`)
for the tools that do this by default if no passphrase is supplied, i.e.
`systemd-dissect`, `systemd-nspawn` and `systemd-sysext`/`systemd-confext`.
It cannot turn this on for other uses (including `systemd-mountfsd`),
which act on behalf of unprivileged clients.
`systemctl`:
* `$SYSTEMCTL_FORCE_BUS=1` — if set, do not connect to PID 1's private D-Bus
+8 -1
View File
@@ -152,7 +152,14 @@
<para>When the OS image contains LUKS encrypted or Verity integrity protected file systems
appropriate volumes are automatically set up and marked for automatic disassembly when the image is
unmounted.</para>
unmounted. LUKS volumes carrying a TPM2 token (as enrolled via the <option>--tpm2-device=</option>
option of
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
are unlocked via the local TPM2 automatically, before a passphrase is requested. This may be turned
off via the <varname>$SYSTEMD_DISSECT_TPM2_UNLOCK</varname> environment variable. Note that this
requires privileges: when invoked unprivileged, images are mounted via
<citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which does not currently support unlocking via TPM2.</para>
<para>The OS image may either be specified as path to an OS image stored in a regular file or may
refer to block device node (in the latter case, the block device must be the "whole" device, i.e. not
+6 -1
View File
@@ -341,7 +341,12 @@
<filename>/efi</filename> (or <filename>/boot</filename> as fallback) in case a directory by this name exists
and is empty.</para>
<para>Partitions encrypted with LUKS are automatically decrypted. Also, on GPT images dm-verity data integrity
<para>Partitions encrypted with LUKS are automatically decrypted, via a TPM2 token enrolled in the LUKS2
header if one matches the local TPM2 (this may be turned off via the
<varname>$SYSTEMD_DISSECT_TPM2_UNLOCK</varname> environment variable), or otherwise by prompting for a
passphrase. Unlocking via TPM2 requires privileges: when invoked unprivileged, images are mounted via
<citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which does not currently support it. Also, on GPT images dm-verity data integrity
hash partitions are set up if the root hash for them is specified using the <option>--root-hash=</option>
option.</para>
+5 -1
View File
@@ -101,7 +101,11 @@
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>
supports via its <option>--directory=</option>/<option>--image=</option> switches and those that the
service manager supports via <option>RootDirectory=</option>/<option>RootImage=</option>. Similar to
them they may optionally carry Verity authentication information.</para>
them they may optionally carry Verity authentication information. LUKS encrypted extension images
carrying a TPM2 token (as enrolled via the <option>--tpm2-device=</option> option of
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
are unlocked via the local TPM2 automatically, without any passphrase. This may be turned off via the
<varname>$SYSTEMD_DISSECT_TPM2_UNLOCK</varname> environment variable.</para>
<para>System extensions are searched for in the directories
<filename>/etc/extensions/</filename>, <filename>/run/extensions/</filename> and
+10
View File
@@ -53,6 +53,16 @@
<citerefentry><refentrytitle>systemd-tpm2-swtpm.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
service.</para>
<para>In the initrd, the generator also orders
<citerefentry><refentrytitle>systemd-sysext-initrd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and
<citerefentry><refentrytitle>systemd-confext-initrd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
after <filename>tpm2.target</filename> and
<citerefentry><refentrytitle>systemd-pcrphase-initrd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
so that LUKS encrypted extension images sealed to the TPM2 can be unlocked. This is not done if the
software TPM fallback is in use, as the software TPM only becomes available after the extension images
are merged, hence such images cannot be unlocked in the initrd in that case.</para>
<para><command>systemd-tpm2-generator</command> implements
<citerefentry><refentrytitle>systemd.generator</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
</refsect1>
+2 -1
View File
@@ -92,7 +92,8 @@ static DissectImageFlags arg_flags =
DISSECT_IMAGE_GROWFS |
DISSECT_IMAGE_PIN_PARTITION_DEVICES |
DISSECT_IMAGE_ADD_PARTITION_DEVICES |
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
DISSECT_IMAGE_TPM2_UNLOCK;
static VeritySettings arg_verity_settings = VERITY_SETTINGS_DEFAULT;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
+4 -4
View File
@@ -295,7 +295,7 @@ int bus_image_method_get_hostname(
int r;
if (!image->metadata_valid) {
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -314,7 +314,7 @@ int bus_image_method_get_machine_id(
int r;
if (!image->metadata_valid) {
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -343,7 +343,7 @@ int bus_image_method_get_machine_info(
int r;
if (!image->metadata_valid) {
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -361,7 +361,7 @@ int bus_image_method_get_os_release(
int r;
if (!image->metadata_valid) {
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
+1 -1
View File
@@ -633,7 +633,7 @@ static int list_image_one_and_maybe_read_metadata(Manager *m, sd_varlink *link,
assert(image);
if (should_acquire_metadata(am) && !image->metadata_valid) {
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0 && am != ACQUIRE_METADATA_GRACEFUL)
return log_debug_errno(r, "Failed to read image metadata: %m");
if (r < 0)
+1
View File
@@ -3877,6 +3877,7 @@ static DissectImageFlags determine_dissect_image_flags(void) {
(arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK|DISSECT_IMAGE_GROWFS) |
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
DISSECT_IMAGE_VERITY_SHARE |
DISSECT_IMAGE_TPM2_UNLOCK |
(arg_console_mode == CONSOLE_INTERACTIVE && arg_ask_password ? DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH : 0) |
((arg_userns_ownership == USER_NAMESPACE_OWNERSHIP_FOREIGN) ? DISSECT_IMAGE_FOREIGN_UID :
(arg_userns_ownership != USER_NAMESPACE_OWNERSHIP_AUTO) ? DISSECT_IMAGE_IDENTITY_UID : 0);
+1 -1
View File
@@ -61,7 +61,7 @@ int bus_image_common_get_os_release(
return 1;
if (!image->metadata_valid) {
r = image_read_metadata(image, /* root= */ NULL, &image_policy_service, m->runtime_scope);
r = image_read_metadata(image, /* root= */ NULL, &image_policy_service, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
+3 -2
View File
@@ -2228,7 +2228,7 @@ int image_setup_pool(RuntimeScope scope, ImageClass class, bool use_btrfs_subvol
return 0;
}
int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope) {
int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope, DissectImageFlags extra_flags) {
_cleanup_(release_lock_file) LockFile global_lock = LOCK_FILE_INIT, local_lock = LOCK_FILE_INIT;
int r;
@@ -2316,7 +2316,8 @@ int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_pol
DISSECT_IMAGE_PIN_PARTITION_DEVICES |
DISSECT_IMAGE_VALIDATE_OS |
DISSECT_IMAGE_VALIDATE_OS_EXT |
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
extra_flags;
r = loop_device_make_by_path(
i->path,
+1 -1
View File
@@ -76,7 +76,7 @@ int image_get_pool_usage(RuntimeScope scope, ImageClass class, uint64_t *ret);
int image_get_pool_limit(RuntimeScope scope, ImageClass class, uint64_t *ret);
int image_setup_pool(RuntimeScope scope, ImageClass class, bool use_btrfs_subvol, bool use_btrfs_quota);
int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope);
int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope, DissectImageFlags extra_flags);
bool image_in_search_path(RuntimeScope scope, ImageClass class, const char *root, const char *image);
+1
View File
@@ -20,6 +20,7 @@ typedef enum BusTransport BusTransport;
typedef enum CatFlags CatFlags;
typedef enum CertificateSourceType CertificateSourceType;
typedef enum ConditionType ConditionType;
typedef enum DissectImageFlags DissectImageFlags;
typedef enum DnsAnswerFlags DnsAnswerFlags;
typedef enum DnsCacheMode DnsCacheMode;
typedef enum DnsOverTlsMode DnsOverTlsMode;
+3 -2
View File
@@ -2068,7 +2068,8 @@ static int merge_subprocess(
DISSECT_IMAGE_USR_NO_ROOT |
DISSECT_IMAGE_ADD_PARTITION_DEVICES |
DISSECT_IMAGE_PIN_PARTITION_DEVICES |
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
DISSECT_IMAGE_TPM2_UNLOCK;
r = verity_settings_load(&verity_settings, img->path, NULL, NULL);
if (r < 0)
@@ -2651,7 +2652,7 @@ static int image_discover_and_read_metadata(const Context *c, Hashmap **ret_imag
return log_error_errno(r, "Failed to discover images: %m");
HASHMAP_FOREACH(img, images) {
r = image_read_metadata(img, c->root, image_class_info[c->image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM);
r = image_read_metadata(img, c->root, image_class_info[c->image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM, DISSECT_IMAGE_TPM2_UNLOCK);
if (r < 0)
return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
}
+41 -1
View File
@@ -12,6 +12,7 @@
#include "path-util.h"
#include "proc-cmdline.h"
#include "special.h"
#include "strv.h"
#include "tpm2-util.h"
/* A small generator that enqueues tpm2.target as synchronization point if the TPM2 device hasn't shown up
@@ -74,6 +75,39 @@ static int generate_tpm_target_symlink(Tpm2Support support, bool software_fallba
return 0;
}
static int generate_initrd_extension_ordering(bool software_fallback_enabled) {
int r;
/* systemd-sysext-initrd.service and systemd-confext-initrd.service might have to unlock LUKS
* encrypted extension images via the TPM2, hence order them after tpm2.target and after the
* "enter-initrd" PCR measurement. This cannot be done statically in the units: they run before
* local-fs-pre.target, but the software TPM fallback only becomes available after the early ESP
* mount, which is ordered after local-fs-pre.target, so with the fallback the ordering would be
* a cycle. Hence add it here, and only if the fallback is not in use. */
if (!in_initrd())
return 0;
if (software_fallback_enabled) {
log_debug("Not ordering initrd extension image units after tpm2.target, as the software TPM fallback is in use.");
return 0;
}
FOREACH_STRING(u, "systemd-sysext-initrd.service", "systemd-confext-initrd.service") {
r = write_drop_in(
arg_dest,
u,
50, "tpm2",
"# Automatically generated by systemd-tpm2-generator\n\n"
"[Unit]\n"
"After=tpm2.target systemd-pcrphase-initrd.service\n");
if (r < 0)
return log_error_errno(r, "Failed to order %s after tpm2.target: %m", u);
}
return 0;
}
static int generate_swtpm_symlink(Tpm2Support support) {
int r;
@@ -158,7 +192,13 @@ static int generate_now(void) {
if (r < 0)
return r;
r = generate_tpm_target_symlink(support, /* software_fallback_enabled= */ r > 0);
bool software_fallback_enabled = r > 0;
r = generate_tpm_target_symlink(support, software_fallback_enabled);
if (r < 0)
return r;
r = generate_initrd_extension_ordering(software_fallback_enabled);
if (r < 0)
return r;
+2
View File
@@ -28,6 +28,8 @@ Conflicts=initrd-switch-root.target
Before=initrd-switch-root.target
Wants=modprobe@loop.service modprobe@dm_mod.service
After=modprobe@loop.service modprobe@dm_mod.service
# Extension images might be LUKS encrypted and sealed to the local TPM2, but ordering after tpm2.target here
# would be a cycle with the software TPM fallback, hence systemd-tpm2-generator adds it where possible
[Service]
Type=oneshot
+2
View File
@@ -23,6 +23,8 @@ Conflicts=shutdown.target
Before=initrd-root-fs.target shutdown.target
Wants=modprobe@loop.service modprobe@dm_mod.service
After=modprobe@loop.service modprobe@dm_mod.service sysroot.mount sysroot-usr.mount systemd-volatile-root.service
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
[Service]
Type=oneshot
+2
View File
@@ -21,6 +21,8 @@ ConditionKernelCommandLine=!systemd.confext=0
DefaultDependencies=no
After=local-fs.target
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
Before=sysinit.target systemd-tmpfiles-setup.service
Conflicts=shutdown.target
Before=shutdown.target
+4
View File
@@ -51,5 +51,9 @@ DeviceAllow=block-blkext rw
DeviceAllow=/dev/mapper/control rw
DeviceAllow=block-device-mapper rw
# nspawn unlocks LUKS encrypted images via a TPM2 token enrolled in them, in
# which case it needs access to the TPM2 resource manager device.
DeviceAllow=/dev/tpmrm0 rw
[Install]
WantedBy=machines.target
+2
View File
@@ -22,6 +22,8 @@ ConditionKernelCommandLine=!rd.systemd.sysext=0
DefaultDependencies=no
After=systemd-confext-initrd.service
# Extension images might be LUKS encrypted and sealed to the local TPM2, but ordering after tpm2.target here
# would be a cycle with the software TPM fallback, hence systemd-tpm2-generator adds it where possible
Before=local-fs-pre.target cryptsetup-pre.target systemd-tmpfiles-setup.service
Wants=local-fs-pre.target cryptsetup-pre.target
Conflicts=initrd-switch-root.target
+2
View File
@@ -23,6 +23,8 @@ Conflicts=shutdown.target
Before=initrd-root-fs.target shutdown.target
Wants=modprobe@loop.service modprobe@dm_mod.service
After=modprobe@loop.service modprobe@dm_mod.service sysroot.mount sysroot-usr.mount systemd-volatile-root.service
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
[Service]
Type=oneshot
+2
View File
@@ -22,6 +22,8 @@ ConditionKernelCommandLine=!systemd.sysext=0
DefaultDependencies=no
After=systemd-confext.service
After=local-fs.target
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
Before=sysinit.target systemd-tmpfiles-setup.service
Conflicts=shutdown.target
Before=shutdown.target