Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package sdbootutil for openSUSE:Factory checked in at 2026-10-20 12:57:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/sdbootutil (Old) and /work/SRC/openSUSE:Factory/.sdbootutil.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "sdbootutil" Tue Oct 20 12:57:08 2026 rev:114 rq:1386348 version:1+git20261007.53bc127 Changes: -------- --- /work/SRC/openSUSE:Factory/sdbootutil/sdbootutil.changes 2026-09-30 16:22:40.803416791 +0200 +++ /work/SRC/openSUSE:Factory/.sdbootutil.new.3698736/sdbootutil.changes 2026-10-20 12:57:16.715117287 +0200 @@ -1,0 +2,6 @@ +Fri Oct 16 06:23:18 UTC 2026 - Alberto Planas Dominguez <[email protected]> + +- Update to version 1+git20261007.53bc127: + * doc: add raw-disk-image architecture document + +------------------------------------------------------------------- @@ -20 +26 @@ -- Update to version 1+git20260924.2b7b94e: +- Update to version 1+git20260924.2b7b94e (bsc#1284183, CVE-2026-104617, bsc#1284178, CVE-2026-104618): Old: ---- sdbootutil-1+git20260929.26b6989.obscpio New: ---- sdbootutil-1+git20261007.53bc127.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ sdbootutil.spec ++++++ --- /var/tmp/diff_new_pack.WDltGT/_old 2026-10-20 12:57:18.243181274 +0200 +++ /var/tmp/diff_new_pack.WDltGT/_new 2026-10-20 12:57:18.245181357 +0200 @@ -55,7 +55,7 @@ %{nil} Name: sdbootutil -Version: 1+git20260929.26b6989 +Version: 1+git20261007.53bc127 Release: 0 Summary: Bootctl wrapper for BLS boot loaders License: MIT ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.WDltGT/_old 2026-10-20 12:57:18.340185336 +0200 +++ /var/tmp/diff_new_pack.WDltGT/_new 2026-10-20 12:57:18.345185545 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/openSUSE/sdbootutil.git</param> - <param name="changesrevision">26b6989e346388bfa244b226c809a1e6a3824a0d</param></service></servicedata> + <param name="changesrevision">53bc1271fcf2b544baafbc9bedd29d0494b9713a</param></service></servicedata> (No newline at EOF) ++++++ sdbootutil-1+git20260929.26b6989.obscpio -> sdbootutil-1+git20261007.53bc127.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20260929.26b6989/doc/raw-disk-image-architecture.md new/sdbootutil-1+git20261007.53bc127/doc/raw-disk-image-architecture.md --- old/sdbootutil-1+git20260929.26b6989/doc/raw-disk-image-architecture.md 1970-01-01 01:00:00.000000000 +0100 +++ new/sdbootutil-1+git20261007.53bc127/doc/raw-disk-image-architecture.md 2026-10-07 14:13:20.000000000 +0200 @@ -0,0 +1,870 @@ +# Raw disk image openSUSE Full Disk Encryption + +This document explains how `disk-encryption-tool`, `jeos-firstboot`, +`sdbootutil`, and (optionally) `combustion` cooperate to deliver Full +Disk Encryption (FDE) with different unlocking mechanisms like +password, recovery key, TPM2 or FIDO2 on openSUSE Tumbleweed and +MicroOS, using `systemd-cryptenroll`, `systemd-pcrlock` and +`cryptsetup`. + +--- + +## 1. Disk enrollment + +When a MicroOS or a Tumbleweed image is booted by the first time, the +new image will be encrypted and enrolled in order to enable FDE. +Those two steps are worked by two different components: + +1. **Converting the unencrypted root filesystem into LUKS2** - always + done by `disk-encryption-tool` (specifically + `disk-encryption-tool-dracut`, run from the first-boot initrd). + This step always generates one *transient, random* recovery + passphrase and enrolls it as a throwaway LUKS2 keyslot called the + **enrollment-key**. +2. **Enrolling the "real" unlock factors** (TPM2, TPM2+PIN, FIDO2, + password, recovery-key) and wiring up `systemd-pcrlock` boot-chain + attestation - always done by `sdbootutil enroll`, either: + - **unattended**, driven by systemd credentials and run once by + `sdbootutil-enroll.service`, or + - **interactively**, driven by dialogs in the + `jeos-firstboot-enroll` module and run as part of + `jeos-firstboot.service`. + +`combustion` (and Ignition, cloud-init, or any other first-boot +provisioning mechanism) is **one way to deliver the systemd +credentials, kernel keys, or environment variables** that select the +unattended path. `jeos-firstboot` is the human-facing alternative +when no such credentials are present. `disk-encryption-tool` always +does the LUKS2 conversion; `combustion` and `jeos-firstboot` are two +alternative ways to complete the TPM2/FIDO2 enrollment step +afterwards. Agama / YaST2 can in the same way deliver the credentials +or kernel keys and call `sdbootutil` to complete the enrollment. + +``` + ┌───────────────────────────────────────────┐ + │ IMAGE BUILD TIME │ + │ kiwi image contains plain-text btrfs root │ + │ + disk-encryption-tool dracut module │ + │ (95disk-encryption-tool, always present) │ + └───────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ FIRST BOOT - initrd stage, all still before switch_root │ + │ │ + │ firstboot-detect.service → firstboot.target (only if this │ + │ really is a first boot: no/uninitialized machine-id, or │ + │ combustion.firstboot/ignition.firstboot on the cmdline) │ + │ │ │ + │ ▼ │ + │ combustion-prepare.service (script "--prepare", waits for the │ + │ config-drive device, may set up networking) │ + │ → e.g. writes /run/credstore/disk-encryption-tool-dracut.encrypt │ + │ = "force" so encryption isn't skipped/aborted │ + │ │ │ + │ ▼ │ + │ combustion.service (script normal phase, run via │ + │ `transactional-update shell` / chroot into the *still │ + │ plain-text* /sysroot - not yet switch_root'ed) │ + │ → e.g. writes /etc/credstore.encrypted/sdbootutil-enroll.* │ + │ (systemd-creds encrypt) into the plain-text filesystem - │ + │ these files survive the in-place LUKS2 conversion below │ + │ unchanged, and are read again after the *next* boot │ + │ │ (After=ignition-complete.target too, if used) │ + │ ▼ │ + │ disk-encryption-tool-dracut.service (RequiredBy=firstboot.target,│ + │ After=combustion.service) │ + │ → disk-encryption-tool-dracut → disk-encryption-tool │ + │ → shrinks btrfs, cryptsetup reencrypt --encrypt in place, │ + │ grows back, writes /etc/crypttab, enrolls a *transient* │ + │ random "enrollment-key" LUKS2 slot into the session keyring│ + │ ( %user:cryptenroll ), regenerates initrd │ + └──────────────────────────────────────────────────────────────────┘ + │ switch_root into the now-encrypted rootfs + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ FIRST BOOT - real root, enrollment stage │ + │ (KeyringMode=shared everywhere so the session keyring with the │ + │ transient enrollment-key survives the switch_root) │ + │ │ + │ PATH A: unattended PATH B: interactive │ + │ (sdbootutil-enroll.* credentials (no sdbootutil-enroll.* │ + │ already present in credentials present) │ + │ /etc/credstore.encrypted, │ + │ written by combustion above) │ + │ │ │ │ + │ ▼ ▼ │ + │ sdbootutil-enroll.service jeos-firstboot.service │ + │ (After=jeos-firstboot.service) → enroll module │ + │ │ │ │ + │ └───────────────┬────────────────────┘ │ + │ ▼ │ + │ sdbootutil enroll --method=<recovery-key|password| │ + │ tpm2[+pin]|fido2> │ + │ → systemd-cryptenroll (+ systemd-pcrlock make-policy │ + │ for tpm2/tpm2+pin) │ + │ → on full success: wipe the transient enrollment-key │ + │ keyslot │ + └──────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ EVERY SUBSEQUENT BOOT │ + │ systemd-cryptsetup unseals the LUKS2 volume key via TPM2 │ + │ (policy = /var/lib/systemd/pcrlock.json) or FIDO2 token │ + │ measure-pcr-validator.service (initrd) independently verifies │ + │ PCR 15 against a signed prediction, for tpm2-measure-pcr=yes │ + │ devices only │ + └──────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ ONGOING SYSTEM LIFECYCLE │ + │ kernel update / snapshot create-default / bootloader update │ + │ → sdbootutil (via 50-sdbootutil.install, 10-sdbootutil.snapper,│ + │ 10-sdbootutil.tukit) sets update_predictions=1 │ + │ → sdbootutil update-predictions (or sdbootutil-update- │ + │ predictions.service) regenerates systemd-pcrlock policy and │ + │ measure-pcr-* prediction so the new boot chain still unlocks│ + └──────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. The components + +### 2.1 `disk-encryption-tool` + +A dracut module (`/usr/lib/dracut/modules.d/95disk-encryption-tool`) +plus a standalone script. Its job is **turn a plain btrfs (or swap) +volume into LUKS2**. + +- `disk-encryption-tool` - the actual worker. Given a mountpoint or + block device, it: + 1. Refuses if already LUKS or if the target volume name already exists. + 2. For btrfs: mounts (or remounts rw), shrinks to `btrfs + inspect-internal min-dev-size`, shrinks the partition table entry + with `sfdisk`, unmounts. For swap: computes a minimal size and + shrinks similarly. + 3. Picks a passphrase: `--key VALUE` (explicit), `--keyring NAME` + (recover/store in kernel keyring `%user:NAME`), or - the normal + first-boot case - generates a fresh random one (`dd + if=/dev/urandom bs=8 count=1 | base64`). + 4. Runs `cryptsetup reencrypt --encrypt --reduce-device-size=32m + --progress-frequency=1 <extra options from /etc/encrypt_options>` + on the raw partition, in place. + 5. Grows the partition and filesystem back to the full device size, + remounts. + 6. Tags the generated key as an `enrollment-key` LUKS2 token + (`cryptsetup token import` with + `{"type":"enrollment-key","keyslots":["0"]}`) - this is the + marker that `sdbootutil-enroll` / `jeos-firstboot-enroll` later + look for and wipe once real enrollment succeeds. + 7. Appends an `/etc/crypttab` entry for the volume. + +- `disk-encryption-tool-dracut` - the initrd glue invoked by the + systemd unit below. It decides *whether* to encrypt at all (systemd + credential + kernel cmdline + a "system already configured?" + heuristic based on `/sysroot/var/lib/YaST2/reconfig_system`), gives + the console a 10-second Esc-to-abort window unless forced, discovers + `root_device` via `findmnt`, optionally encrypts extra partitions + listed in a credential, then encrypts `/sysroot` by calling + `disk-encryption-tool` itself. + +- `module-setup.sh` - the dracut module descriptor: depends on + dracut's `crypt` module, pulls in `dmi_sysfs` (so systemd + credentials delivered via QEMU `fw_cfg`/SMBIOS reach the initrd), + installs `cryptsetup`, `cryptsetup-reencrypt`, `btrfs`, `sfdisk`, + etc., ships both scripts to `/usr/bin/`, installs and **enables** + `disk-encryption-tool-dracut.service` inside the built initrd, and + optionally bakes a build-host `/etc/encrypt_options` into the image. + +The package `Requires: combustion cryptsetup keyutils` - Combustion is +a hard runtime dependency because +`disk-encryption-tool-dracut.service` is explicitly ordered +`After=combustion.service` (and `After=ignition-complete.target`), so +that a Combustion/Ignition script gets to run first and set +credentials such as `disk-encryption-tool-dracut.encrypt=force` before +the encryption decision is made. Since `combustion.service` itself +already `Requires=`/`After=combustion-prepare.service` (§2.4), +depending on it alone is enough to guarantee *both* of combustion's +script invocations have finished. + +### 2.2 `jeos-firstboot` + +A generic first-boot text wizard. The FDE support is delivered via a +plugin (described later). + +- `jeos-firstboot.service` runs once, gated by + `ConditionPathExists=/var/lib/YaST2/reconfig_system` (set by + image-build tooling), masks the stock `systemd-firstboot.service` + (jeos-firstboot calls the `systemd-firstboot` *binary* itself once + locale/keymap/timezone dialogs finish), and imports + `ImportCredential=passwd.plaintext-password.root` + + `ImportCredential=firstboot.*`. +- It discovers modules from `/etc/jeos-firstboot/modules/*` (wins) and + `/usr/share/jeos-firstboot/modules/*` (fallback), skips any module + that is a symlink to `/dev/null`, sorts by a `<module>_priority` + property (default `50`), and calls, in that order: + `<module>_systemd_firstboot` (before/around the real + `systemd-firstboot` call), then `<module>_post` (after), then + `<module>_cleanup` (on exit). +- `jeos-config` reuses the same module machinery to offer a standalone + reconfiguration menu (`<module>_jeos_config` hook). +- `jeos-firstboot-snapshot.service` runs after + `jeos-firstboot.service`, deletes the `reconfig_system` flag (the + actual "only runs once" mechanism - both units share the same + `ConditionPathExists`, so once the flag is gone, neither runs + again), and takes a snapper snapshot of the freshly configured + system. + +FDE support is added *externally*, by `sdbootutil` shipping a module +named `enroll` (`jeos-firstboot-enroll`, described in §2.3) into +`/usr/share/jeos-firstboot/modules/`. + +### 2.3 `sdbootutil` - enrollment & PCR-lock subsystem + +`sdbootutil` is primarily a bootloader manager (see its own +`ARCHITECTURE.md`), but it owns *all* FDE enrollment and +PCR-prediction logic. The FDE-relevant pieces: + +| File | Role | +|---|---| +| `sdbootutil` (main script) | `enroll`, `unenroll-device`, `list-devices`, `update-predictions` subcommands; all `pcrlock*`/`measure_pcr*`/`enroll_*` functions | +| `sdbootutil-enroll` | Standalone orchestrator: reads `sdbootutil-enroll.*` credentials/keyring and drives unattended enrollment | +| `sdbootutil-enroll.service` | Runs `sdbootutil-enroll` once at first boot, after `jeos-firstboot.service` | +| `jeos-firstboot-enroll` | jeos-firstboot module: interactive TPM2/FIDO2/password/recovery-key dialogs | +| `jeos-firstboot-enroll-override.conf` | Drop-in on `jeos-firstboot.service` adding `KeyringMode=shared` | +| `measure-pcr-generator.sh` | systemd generator: uses `SYSTEMD_FORCE_MEASURE` to enable volume key measurement in PCR15 and order devices | +| `measure-pcr-validator.sh` / `.service` | Initrd-time validation of the signed PCR15 prediction | +| `sdbootutil-update-predictions.service` | Regenerates `systemd-pcrlock` policy prediction after system changes | +| `kernel-install-sdbootutil.conf` | tmpfiles drop-in disabling stock `kernel-install` scripts that would otherwise bypass sdbootutil's prediction updates | +| `snapper-override.conf`, `10-sdbootutil.tukit(.conf)` | Ensure snapshot/transactional-update operations can regenerate predictions correctly | + +### 2.4 `combustion` + +A dracut module (depends on `bash firstboot network systemd url-lib`) +that runs a single user-provided script, once, on first boot - +entirely from *within the initrd*, before `switch_root`. It has no +built-in knowledge of encryption, credentials, or `sdbootutil`; it is +purely a generic "run this script at boot, twice, at two different +points" mechanism that other tools (here, `disk-encryption-tool` and, +indirectly, `sdbootutil`) are wired up to via ordering +(`After=combustion.service`) and via the script's own contents. + +- **Gating**: the separate `30firstboot` dracut module's + `firstboot-detect.service` decides whether this is actually a first + boot (missing/`uninitialized` `/etc/machine-id`, or + `combustion.firstboot=`/ `ignition.firstboot=` on the kernel + cmdline) and only then activates `firstboot.target`, which pulls in + `combustion.service` (`RequiredBy=firstboot.target`). On a normal + (non-first) boot, none of this runs. +- **Config discovery**: in priority order - `combustion.url=` kernel + cmdline parameter (fetched with `curl`), a QEMU `fw_cfg` blob + (`opt/org.opensuse.combustion/script`), a VMware + `guestinfo.combustion.script` key (base64+gzip), or a block device + with `LABEL=combustion`/`COMBUSTION` (falling back to + `ignition`/`IGNITION`/`install`/`INSTALL` labels for + co-installability with Ignition or a KIWI self-install ISO). + `combustion.rules` (udev) aliases whichever source is found to a + synthetic `/dev/combustion/config` device, with a 10s (30s on + aarch64) detection timeout. +- **Two invocations, both pre-switch_root**: + 1. `combustion-prepare.service` runs `/usr/bin/combustion + --prepare`, which execs the script as `./script --prepare` - + still in the initrd, before networking/the initqueue is even + brought up. A script opts into this by including a line matching + `^# combustion:(.*)\bprepare\b` anywhere in the file (commonly, + but not strictly required to be, the first line); the + `network`/`prepare` flags can be combined on one such line, + e.g. `# combustion: network prepare`. + 2. `combustion.service` runs `/usr/bin/combustion --complete`, + which - if `transactional-update` is available - execs the plain + script (no arguments) inside a `transactional-update shell`, i.e. + chrooted into `/sysroot`, which at this point is **still the + plain-text filesystem** (`disk-encryption-tool-dracut.service` + hasn't run yet). If the script exits non-zero here, the whole + transaction is rolled back and `combustion.service` fails, which + fails the boot - a script that must write a credential should + treat a failed write as fatal, not swallow the error. +- **No built-in credential support**: combustion has no dedicated verb + or helper for systemd credentials, `/run/credstore`, or + `/etc/credstore.encrypted` - a script has to call + `systemd-creds`/write files itself, exactly as shown in §3.1's + examples. A missing config source/script is only a warning, not a + boot failure. + +Because the "normal phase" chroot happens *before* +`disk-encryption-tool-dracut.service` converts `/sysroot` to LUKS2, +anything a combustion script writes into the filesystem at that point +(e.g. `/etc/credstore.encrypted/sdbootutil-enroll.pw`) is preserved +as-is by the in-place `cryptsetup reencrypt` and is still there, +readable after `switch_root`, on every later boot - which is exactly +why the *persistent* `sdbootutil-enroll.*` credentials use +`/etc/credstore.encrypted` while the *this-boot-only* +`disk-encryption-tool-dracut.encrypt` decision uses the transient, +initrd-local `/run/credstore` instead (only +`combustion-prepare.service`'s earlier, still-in-initrd invocation can +usefully set that one, since `disk-encryption-tool-dracut.service` +also runs before `switch_root`). + +--- + +## 3. The three ways FDE gets enabled + +### 3.1 Via `combustion` (fully unattended) + +Both of combustion's script invocations happen in the initrd, before +`switch_root` (see §2.4) - there is no separate combustion stage after +the real root has taken over: + +1. **`--prepare` phase (`combustion-prepare.service`), before + `disk-encryption-tool-dracut.service` even starts.** Typical use: + force encryption without the interactive abort countdown, via the + *transient* `/run/credstore`: + ```sh + #!/bin/bash + # combustion: prepare + if [ "$1" = "--prepare" ]; then + mkdir -p /run/credstore + echo "force" > /run/credstore/disk-encryption-tool-dracut.encrypt + exit 0 + fi + ``` + (The `# combustion: prepare` marker just has to appear on some line + starting with `# combustion:` - the actual match is `grep -qE '^# + combustion:(.*)\bprepare\b'` against the whole file, not strictly + the first line - but putting it first, as shown, is the documented + convention.) +2. **Normal phase (`combustion.service`), also still + pre-`switch_root`**, executed via `transactional-update shell` + chrooted into `/sysroot` - which at this point is **still the + plain-text filesystem**, since + `disk-encryption-tool-dracut.service` (ordered + `After=combustion.service`) hasn't run yet. Typical use: seed the + *persistent* `sdbootutil-enroll.*` credentials so enrollment is + fully unattended once the system reaches its real root, on a later + boot: + ```sh + mkdir -p /etc/credstore.encrypted + echo "linux" | systemd-creds encrypt --name=sdbootutil-enroll.pw - \ + /etc/credstore.encrypted/sdbootutil-enroll.pw + # ...similarly for sdbootutil-enroll.tpm2, .tpm2+pin, .fido2, .rk, .recovery-pin + ``` + Because this file lands in the plain-text root *before* it is + converted to LUKS2, it survives the in-place `cryptsetup reencrypt` + unchanged and is still present (now on the encrypted volume) after + `switch_root`. `sdbootutil-enroll.service`'s + `ImportCredential=sdbootutil-enroll.*` picks it up automatically + the next time it runs - no interactive dialog is shown at all, and + `jeos-firstboot-enroll`'s dialog is effectively skipped in practice + because `sdbootutil-enroll` (or the module itself, depending on + which credentials exist) has already enrolled the requested + method(s) and wiped the transient key. + + Nothing stops other first-boot tools (Ignition, cloud-init) from + writing the very same credential files - Combustion is the + mechanism actually exercised in this repo's own test suite + (`disk-encryption-tool/test/testscript`). Note also that a + combustion script's normal phase failing (non-zero exit) triggers a + `transactional-update rollback` and fails `combustion.service`, + which fails the boot - so a script that must write one of these + credentials should treat a failed write as fatal. + +### 3.2 Via `disk-encryption-tool` (manual/offline, or as the always-present base layer) + +`disk-encryption-tool` performs the actual LUKS2 conversion, +regardless of which enrollment path is used afterwards (at least for +image based installations, as Agama / YaST2 can do the same for +traditional installations). + +### 3.3 Via `jeos-firstboot` (interactive, local installs) + +If no `sdbootutil-enroll.*` credential is present, the `enroll` module +(`jeos-firstboot-enroll`) in `jeos-firstboot.service` presents a +text-mode menu once the transient enrollment key is confirmed present +in the session keyring (`keyctl id` lookup for `%user:cryptenroll`): + +- Detects available factors: FIDO2 (`systemd-cryptenroll + --fido2-device=list`), TPM2 (`/sys/class/tpm/tpm0`). +- Lets the admin choose, repeatedly, from: recovery-key, FIDO2, TPM2 + (with an optional, interactive, double-entry PIN sub-flow), reuse + the root password, or set an extra password - until they select + "done". +- On the module's `_post` hook (after `systemd-firstboot` itself has + run), calls `sdbootutil enroll --method=...` once per selected + method, in the fixed order recovery-key → password → tpm2[+pin] → + fido2, exactly mirroring `sdbootutil-enroll`'s order (the two share + an identical `wipe_enrollment_key()` implementation, deliberately + kept in sync per a code comment). +- Displays the generated recovery key / PIN via `/run/issue.d/*.issue` + (so it survives to the login banner) before the transient + enrollment-key keyslot is wiped. + +--- + +## 4. End-to-end boot sequence + +**Stage 0 - image build.** A kiwi image ships a plain-text btrfs root +plus the `95disk-encryption-tool` dracut module baked in +(`disk-encryption-tool.spec` installs it purely via `module-setup.sh` +at `dracut`-build time; there is no RPM `%post` scriptlet). +`/var/lib/YaST2/reconfig_system` is present so `jeos-firstboot` will +run. + +**Stage 1 - first-boot initrd, entirely before `switch_root`.** +`firstboot-detect.service` (from the `30firstboot` dracut module) +first decides whether this is actually a first boot at all; if so it +activates `firstboot.target`, which pulls in (in order): +1. `combustion-prepare.service` - runs the combustion script's + `--prepare` invocation (§2.4/§3.1), e.g. to force encryption via + the transient `/run/credstore/disk-encryption-tool-dracut.encrypt`. +2. `combustion.service` - runs the script's normal invocation, + chrooted into the *still plain-text* `/sysroot` via + `transactional-update shell`, e.g. to seed persistent + `/etc/credstore.encrypted/sdbootutil-enroll.*` credentials. +3. `disk-encryption-tool-dracut.service` (`Type=oneshot`, + `DefaultDependencies=false`, `RequiredBy=firstboot.target`, + `After=initrd-root-device.target combustion.service + ignition-complete.target`, `Before=initrd-parse-etc.service`) runs + `disk-encryption-tool-dracut`, which: + - Reads credential `disk-encryption-tool-dracut.encrypt` (`no` ⇒ + skip entirely; `force` ⇒ skip the "already configured" heuristic + and the abort countdown). + - Reads credential `disk-encryption-tool-dracut.partitions` for any + extra (non-root) volumes to encrypt. + - Encrypts `/sysroot` via `disk-encryption-tool` **in place**, + preserving whatever combustion wrote to the filesystem in step 2 + (including the `sdbootutil-enroll.*` credential files), writes + `/etc/crypttab`, and leaves a transient `enrollment-key` LUKS2 + token and its passphrase in the shared session keyring + (`KeyringMode=shared` on the unit). + +**Stage 2 - switch_root, enrollment.** After `switch_root` into the +now-encrypted root, `KeyringMode=shared` on every downstream unit +keeps the transient key visible: +- **Unattended path**: `sdbootutil-enroll.service` + (`After=jeos-firstboot.service`, + `ImportCredential=sdbootutil-enroll.*`) runs `sdbootutil-enroll`, + which enrolls every method for which a credential (or matching + keyring entry) exists, then wipes the transient keyslot on full + success. +- **Interactive path**: `jeos-firstboot.service` (with the + `jeos-firstboot-enroll-override.conf` drop-in adding + `KeyringMode=shared`) runs the `enroll` module's dialogs, then calls + the same `sdbootutil enroll --method=...` sequence from its `_post` + hook. + +Either way, `sdbootutil enroll` for `tpm2`/`tpm2+pin`: +1. Generates (if not already present) an RSA-4096 keypair for PCR15 + measurement signing (`create_measure_pcr_keys`, only if + `--no-measure-pcr` is not given). +2. Adds `tpm2-device=auto` (+ `tpm2-measure-pcr=yes` unless disabled) + to the device's `/etc/crypttab` line. +3. Regenerates the initrd (forced, so the new public key and crypttab + option land in it). +4. Builds the `systemd-pcrlock` policy for the current boot chain + (`generate_tpm2_predictions_pcrlock` → `systemd-pcrlock + ... make-policy`, writing `/var/lib/systemd/pcrlock.json`). +5. Runs `systemd-cryptenroll --wipe-slot=tpm2 --tpm2-device=auto + --tpm2-public-key= --tpm2-pcrlock=/var/lib/systemd/pcrlock.json + [--tpm2-with-pin=1] "$dev"`. +6. Enables `sdbootutil-update-predictions.service` for future changes. + +For `fido2`, it is simply `systemd-cryptenroll --wipe-slot=fido2 +--fido2-device=auto "$dev"` - no PCR involvement at all. + +**Stage 3 - every subsequent boot.** `systemd-cryptsetup` (generated +per `/etc/crypttab` line) unseals the LUKS2 volume key using the TPM2 +device and the `pcrlock.json` policy (or the FIDO2 token, or a +password/recovery-key prompt). Independently, for any device with +`tpm2-measure-pcr=yes`, `measure-pcr-validator.service` (initrd, +`Before=initrd-root-device.target`, +`FailureAction=poweroff-immediate`) verifies the signed PCR15 +prediction - see §6. + +**Stage 4 - ongoing lifecycle.** Kernel installs +(`50-sdbootutil.install` via `kernel-install`), snapshot +create/delete/set-default (`10-sdbootutil.snapper`), and +transactional-update transactions (`10-sdbootutil.tukit`) all funnel +back into `sdbootutil`, which sets an internal `update_predictions=1` +flag whenever the boot chain changes. At the end of the run - or +explicitly via `sdbootutil update-predictions` / +`sdbootutil-update-predictions.service` - the `systemd-pcrlock` policy +and the PCR15 prediction are regenerated so the new +kernel/initrd/entry still unlocks. + +--- + +## 5. systemd units involved + +| Unit | Type | Where it runs | Purpose | +|---|---|---|---| +| `combustion-prepare.service` | oneshot | first-boot initrd, pre-network | Runs the user script's `--prepare` invocation | +| `combustion.service` | oneshot | first-boot initrd, chrooted into plain-text `/sysroot` | Runs the user script's normal invocation | +| `disk-encryption-tool-dracut.service` | oneshot | first-boot initrd | Runs the LUKS2 conversion (§2.1) | +| `jeos-firstboot.service` | oneshot | real root, first boot | Interactive wizard; runs the `enroll` module's dialogs | +| `jeos-firstboot-snapshot.service` | oneshot | real root, first boot | Snapper snapshot after configuration; clears the `reconfig_system` flag | +| `sdbootutil-enroll.service` | oneshot | real root, first boot | Unattended enrollment driven by `sdbootutil-enroll.*` credentials | +| `measure-pcr-validator.service` | oneshot | every boot, initrd | Verifies signed PCR15 prediction before `initrd-root-device.target` | +| `sdbootutil-update-predictions.service` | exec | on demand / triggered | Regenerates `systemd-pcrlock` policy + PCR15 prediction, with backup/restore of `/var/lib/pcrlock.d` on failure | +| `measure-pcr-generator.sh` | dracut generator (not a service) | initrd generation time | Adds `SYSTEMD_FORCE_MEASURE=yes` to relevant `[email protected]` units | + +Full unit files (verbatim from the repos): + +**`combustion-prepare.service`** +```ini +[Unit] +Description=Combustion (preparations) +DefaultDependencies=false +Wants=dev-combustion-config.device +After=dev-combustion-config.device +After=ignition-setup-user.service +Before=ignition-enable-network.service +Before=dracut-initqueue.service nm-initrd.service NetworkManager-initrd.service +Conflicts=initrd-switch-root.target umount.target +Conflicts=dracut-emergency.service emergency.service emergency.target + +[Service] +Type=oneshot +StandardOutput=journal+console +ExecStart=/usr/bin/combustion --prepare +``` +No `[Install]` section - it is never enabled on its own, only pulled +in via `combustion.service`'s `Requires=`. It blocks (up to a 10s/30s +timeout drop-in on `dev-combustion-config.device`) until a config +source is detected by `combustion.rules`, and is explicitly ordered +before networking comes up (`Before=dracut-initqueue.service ...`) so +a `--prepare` script gets to configure things (including writing +credentials) before network services start. + +**`combustion.service`** +```ini +[Unit] +Description=Combustion +DefaultDependencies=false +Requires=initrd-root-device.target +After=initrd-root-device.target +Requires=combustion-prepare.service +After=combustion-prepare.service +After=network-online.target +Wants=network-online.target +After=ignition-complete.target +Before=initrd-parse-etc.service +Before=firstboot.target +Conflicts=initrd-switch-root.target umount.target +Conflicts=dracut-emergency.service emergency.service emergency.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/combustion --complete + +[Install] +RequiredBy=firstboot.target +``` +Runs the script's normal invocation inside `transactional-update +shell` (chrooted into `/sysroot`, still plain-text at this point). +`Requires=`/`After=combustion-prepare.service` means anything ordered +`After=combustion.service` (like +`disk-encryption-tool-dracut.service`) is guaranteed to run after +*both* combustion invocations. Like `combustion-prepare.service`, it +is entirely an initrd unit (`Conflicts=initrd-switch-root.target`, +`Before=initrd-parse-etc.service`) - it never runs post-`switch_root`. + +**`disk-encryption-tool-dracut.service`** +```ini +[Unit] +Description=Encrypt root disk +DefaultDependencies=false +Requires=initrd-root-device.target +After=initrd-root-device.target +After=combustion.service +After=ignition-complete.target +Before=initrd-parse-etc.service +Conflicts=initrd-switch-root.target umount.target +Conflicts=dracut-emergency.service emergency.service emergency.target +OnFailure=emergency.target +OnFailureJobMode=isolate + +[Service] +Type=oneshot +KeyringMode=shared +ExecStart=/usr/bin/disk-encryption-tool-dracut +ImportCredential=disk-encryption-tool-dracut.* + +[Install] +RequiredBy=firstboot.target +``` + +**`jeos-firstboot.service`** +```ini +[Unit] +Description=SUSE JeOS First Boot Wizard +After=apparmor.service local-fs.target plymouth-start.service YaST2-Second-Stage.service +Conflicts=plymouth-start.service [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] +Before=display-manager.service +ConditionPathExists=/var/lib/YaST2/reconfig_system +OnFailure=poweroff.target +Before=wicked.service systemd-user-sessions.service +After=NetworkManager.service +After=cloud-init-local.service +Wants=jeos-firstboot-snapshot.service + +[Service] +Type=oneshot +Environment=TERM=linux +RemainAfterExit=yes +ExecStartPre=/bin/sh -c "/usr/bin/plymouth quit 2>/dev/null || :" +ExecStart=/usr/sbin/jeos-firstboot +StandardOutput=tty +StandardInput=tty +KeyringMode=shared +ImportCredential=passwd.plaintext-password.root +ImportCredential=firstboot.* + +[Install] +WantedBy=default.target +``` +(`KeyringMode=shared` here is actually applied via the +`jeos-firstboot-enroll-override.conf` drop-in shipped by `sdbootutil`, +not the base `jeos-firstboot` package - without it the module can't +see the transient enrollment key created back in the initrd.) + +**`jeos-firstboot-enroll-override.conf`** (drop-in for `jeos-firstboot.service`) +```ini +[Service] +KeyringMode=shared +``` + +**`sdbootutil-enroll.service`** +```ini +[Unit] +Description=Enroll encrypted root disk +DefaultDependencies=false +After=jeos-firstboot.service + +[Service] +Type=oneshot +RemainAfterExit=yes +KeyringMode=shared +ExecStart=/usr/bin/sdbootutil-enroll +ImportCredential=sdbootutil-enroll.* + +[Install] +WantedBy=default.target +``` + +**`measure-pcr-validator.service`** +```ini +[Unit] +Description=Validate LUKS2 devices +DefaultDependencies=false +FailureAction=poweroff-immediate +ConditionPathExists=/etc/crypttab +Wants=cryptsetup.target +After=cryptsetup.target +Before=initrd-root-device.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/measure-pcr-validator +Environment=TERM=linux +ExecStopPost=/bin/sh -c "/usr/bin/plymouth quit 2>/dev/null || :" +StandardOutput=journal+console +StandardInput=null + +[Install] +WantedBy=initrd-root-device.target +``` + +**`sdbootutil-update-predictions.service`** +```ini +[Unit] +Description=Update TPM predictions +ConditionSecurity=tpm2 + +[Service] +Type=exec +KeyringMode=shared +PrivateTmp=yes +ExecCondition=/usr/bin/sh -c '[ -e /etc/crypttab ] && grep -q tpm2-device /etc/crypttab' +ExecStartPre=/usr/bin/sh -c 'mkdir -p "/tmp/$INVOCATION_ID"; [ ! -d /var/lib/pcrlock.d ] || cp -a /var/lib/pcrlock.d "/tmp/$INVOCATION_ID/pcrlock.d"' +ExecStart=/usr/bin/sh -c '\ + systemctl --quiet is-active sdbootutil-update-predictions-shutdown.service && systemctl stop sdbootutil-update-predictions-shutdown.service; \ + while true; do \ + rm -f /run/sdbootutil/update-predictions; \ + /usr/bin/sdbootutil -v update-predictions > /dev/null || exit $?; \ + [ -f /run/sdbootutil/update-predictions ] || break; \ + done' +ExecStopPost=/usr/bin/sh -c '\ + if [ "$SERVICE_RESULT" != "success" ] && [ "$SERVICE_RESULT" != "exec-condition" ]; then \ + if [ -d "/tmp/$INVOCATION_ID/pcrlock.d" ]; then \ + rm -rf /var/lib/pcrlock.d/*; \ + cp -a "/tmp/$INVOCATION_ID/pcrlock.d/." /var/lib/pcrlock.d/; \ + fi; \ + fi; \ + rm -rf "/tmp/$INVOCATION_ID"' +ImportCredential=sdbootutil-update-predictions.* +KillSignal=SIGCONT +TimeoutStopSec=120 + +[Install] +WantedBy=default.target +``` +Enabled automatically by `enroll_tpm2()` the first time TPM2 +enrollment succeeds. Note the backup/restore dance around +`/var/lib/pcrlock.d`: if regeneration fails partway, the previous, +known-good pcrlock component set is restored so the machine doesn't +end up unable to unseal its own disk. + +--- + +## 6. The two independent PCR mechanisms + +It's easy to conflate these; they are **separate, complementary** +checks operating on different data: + +### 6.1 `systemd-pcrlock` - boot-chain attestation for the TPM2 *keyslot itself* + +This is what actually gates whether the TPM2 will release the sealed +secret. `sdbootutil` builds a per-machine "pcrlock" policy (a set of +authorized/expected event-log measurements - firmware, Secure Boot +state/authority, GPT, bootloader PE binaries, kernel command line, +initrd, boot entry) via `systemd-pcrlock lock-*` subcommands, wrapped +by `pcrlock()`/`pcrlock_lock()` in the main `sdbootutil` script, then +folded together with `systemd-pcrlock make-policy` into +`/var/lib/systemd/pcrlock.json`. This is handed to +`systemd-cryptenroll` as +`--tpm2-pcrlock=/var/lib/systemd/pcrlock.json` at TPM2 enrollment +time, and to `systemd-cryptsetup` implicitly at unlock time via the +LUKS2 token metadata. Default PCR set: `0,2,4,7,9` for systemd-boot, +`0,2,4,7,8,9` for grub2-bls (PCR 0/2 - firmware - are dropped under +virtualization). This mechanism supersedes the older, now-removed +`pcr-oracle` static-signed-policy approach referenced in the 2023 +MicroOS blog post; the current code explicitly passes +`--tpm2-public-key=` (empty) to disable that legacy path. + +Whenever the boot chain changes (new kernel, new snapshot default, +bootloader update, Secure Boot authority change), this policy must be +regenerated - that's what `sdbootutil +update-predictions`/`sdbootutil-update-predictions.service` does. + +### 6.2 `measure-pcr-*` - PCR15 device/volume-key binding check + +This is an **sdbootutil-specific, additional** integrity check, +layered on top of (2), that protects against the LUKS device/keyslot +mapping being swapped underneath an otherwise-valid pcrlock policy +(e.g. an attacker presenting a different, TPM2-pcrlock-sealed volume +as `root`). It only applies to crypttab entries with +`tpm2-measure-pcr=yes` (set automatically by `sdbootutil enroll +--method=tpm2[+pin]` unless `--no-measure-pcr` is passed): + +- `measure-pcr-generator.sh` is a **dracut generator**, run + automatically during initrd assembly (not a runtime service). For + every `systemd-cryptsetup@<name>.service` matching a + `tpm2-measure-pcr=yes` crypttab line, it drops + `Environment=SYSTEMD_FORCE_MEASURE=yes` plus ordering between + successive cryptsetup units, so PCR15 gets extended, at unlock time, + with an HMAC of each device's real LUKS volume key (via the Rust + `uhmac` helper for sha1/sha256/sha384/sha512), in a deterministic + order. +- After enrollment (or any crypttab change), `sdbootutil` + (`generate_tpm2_predictions_pcr_15`) computes the expected final + PCR15 value offline the same way, and **signs it** with a dedicated + RSA-4096 keypair (`create_measure_pcr_keys`, `openssl genrsa + ... 4096`, stored at + `/var/lib/sdbootutil/measure-pcr-{private,public}.pem`; only the + public half ships in the initrd), writing + `/var/lib/sdbootutil/measure-pcr-prediction` + `.sha256` and copying + both to the ESP. +- At boot, `measure-pcr-validator.sh`/`.service` verifies the RSA + signature over the prediction, reads the live PCR15 value from + `/sys/class/tpm/tpm0/pcr-<bank>/15` (strongest bank first), and + compares. + - Match → boot continues. + - Mismatch → `measure-pcr-validator.ignore=yes|1|true` on the kernel + command line (`getargbool no measure-pcr-validator.ignore`) + downgrades this to a printed warning; otherwise the unit exits + non-zero, `FailureAction=poweroff-immediate` fires, and the + machine halts rather than booting with a validated-but-unexpected + key mapping. + - Absent PCR15/no `tpm2-measure-pcr=yes` entries → the check is + silently skipped (`exit 0`), so this mechanism is opt-in and + non-fatal by default for systems that don't use it. + +In short: **pcrlock answers "did the correct, expected boot chain +run?"**; **measure-pcr-15 answers "is the volume I just unlocked +actually the one the signed prediction says it should be?"**. Both +are regenerated together by `generate_tpm2_predictions()`/`sdbootutil +update-predictions`, but they write to, and are checked against, +entirely separate state (`/var/lib/systemd/pcrlock.json` vs. +`/var/lib/sdbootutil/measure-pcr-*`). + +--- + +## 7. systemd credentials reference + +All credentials are read either from `$CREDENTIALS_DIRECTORY` +(systemd's tmpfs credential store, populated from +`ImportCredential=`/`LoadCredential=`/`SetCredential=`, +`systemd.set_credential=` on the kernel cmdline, an SMBIOS/`fw_cfg` +OEM string on QEMU, or a file under `/etc/credstore(.encrypted)/`) or, +for the interactive path, the kernel session keyring (works because +`KeyringMode=shared` is set on every relevant unit so the +initrd-created keyring survives `switch_root`). + +| Credential / keyring key | Consumer | Format / meaning | +|---|---|---| +| `disk-encryption-tool-dracut.encrypt` | `disk-encryption-tool-dracut` | `no` = skip encryption entirely; `force` = skip the "already configured" heuristic and the 10 s abort countdown; unset = default heuristic-driven behavior | +| `disk-encryption-tool-dracut.partitions` | `disk-encryption-tool-dracut` | Multi-line list, one `volume_name device options` triplet per line; extra (non-root) volumes to encrypt, or an override for the root volume's name/options | +| `passwd.plaintext-password.root` | `jeos-firstboot.service` | Root password; if present, the interactive root-password dialog is skipped | +| `firstboot.locale` | `jeos-firstboot` | Skips the locale dialog | +| `firstboot.keymap` | `jeos-firstboot` | Skips the keyboard layout dialog | +| `firstboot.license-agreed` | `jeos-firstboot` | Skips the EULA dialog | +| `firstboot.timezone` | `jeos-firstboot` | Skips the timezone dialog | +| `sdbootutil-enroll.current-pw` | `sdbootutil-enroll` | Existing password/passphrase needed to authorize enrolling a *new* factor on an already-enrolled device | +| `sdbootutil-enroll.pw` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a password keyslot | +| `sdbootutil-enroll.rk` | `sdbootutil-enroll` | Enroll a recovery-key keyslot | +| `sdbootutil-enroll.tpm2` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a TPM2 keyslot (no PIN) | +| `sdbootutil-enroll.tpm2+pin` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a TPM2 keyslot; value is the PIN | +| `sdbootutil-enroll.fido2` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a FIDO2 keyslot | +| `sdbootutil-enroll.recovery-pin` | `sdbootutil-enroll` | Recovery PIN for the `systemd-pcrlock` NV-index (lets a recovery-key holder re-authorize/re-lock the pcrlock policy later, e.g. after a key compromise) | +| `sdbootutil-update-predictions.*` | `sdbootutil-update-predictions.service` | Same import-glob pattern, e.g. to supply a recovery PIN during unattended, scheduled prediction refreshes | +| `%user:cryptenroll` (kernel keyring, not a systemd credential) | `disk-encryption-tool` → `sdbootutil enroll`, `jeos-firstboot-enroll` | The transient random passphrase generated during Stage 1; consumed by `require_unlock()` to authorize enrolling the first real factor, then the corresponding LUKS2 slot is wiped | +| `%user:sdbootutil-pw`, `%user:sdbootutil-tpm2-pin`, `%user:sdbootutil-recovery-pin` | `sdbootutil enroll` | Interactive-path equivalents of the `sdbootutil-enroll.*` credentials, set by `jeos-firstboot-enroll`'s dialogs instead of by an external provisioning tool | +| `cryptenroll.tpm2-pin` | `systemd-cryptenroll` (not set anywhere by these repos) | Standard systemd credential for an unattended TPM2-PIN unlock; mentioned in a `sdbootutil` comment only to explain why a PIN-protected TPM2 slot can't self-authorize a *new* enrollment unattended | + +--- + +## 8. Key on-disk state + +| Path | Written by | Meaning | +|---|---|---| +| `/etc/crypttab` | `disk-encryption-tool` (initial), `sdbootutil enroll` (adds `tpm2-device=`/`fido2-device=`/`tpm2-measure-pcr=` options) | Standard crypttab; drives both `[email protected]` generation and `measure-pcr-generator.sh` | +| `/etc/encrypt_options` | Admin / build host | Extra `cryptsetup reencrypt` arguments, one per non-`#`-containing line (e.g. `--type=luks1`, `--iter-time=2000`) | +| `/etc/credstore.encrypted/*` | Combustion's normal-phase script (or Ignition/cloud-init equivalent), via `systemd-creds encrypt`, written into the still plain-text root before LUKS2 conversion | Persistent, encrypted systemd credentials - mainly `sdbootutil-enroll.*` | +| `/run/credstore/*` | Combustion's `--prepare`-phase script, in the initrd | Transient (this-boot-only) credentials - mainly `disk-encryption-tool-dracut.encrypt` | +| `/var/lib/systemd/pcrlock.json` | `systemd-pcrlock make-policy` (via `sdbootutil`) | The TPM2 boot-chain policy handed to `systemd-cryptenroll --tpm2-pcrlock=` | +| `/var/lib/pcrlock.d/*.pcrlock.d/*.pcrlock` | `sdbootutil` `pcrlock_*` functions | Individual pcrlock components (per bootloader entry / kernel / cmdline variation) fed into `make-policy` | +| `/var/lib/sdbootutil/measure-pcr-{private,public}.pem` | `sdbootutil create_measure_pcr_keys` | RSA-4096 keypair signing the PCR15 prediction; only the public key is copied into the initrd/ESP | +| `/var/lib/sdbootutil/measure-pcr-prediction[.sha256]` | `sdbootutil generate_tpm2_predictions_pcr_15` | Signed, expected PCR15 value; validated at boot by `measure-pcr-validator` | +| `/var/lib/sdbootutil/crypttab.sha1` | `sdbootutil` | Tracks whether `/etc/crypttab` changed since the last prediction run | +| `/var/lib/misc/transactional-update.state` | `sdbootutil` | State carried across reboots on transactional (read-only root) systems | + +--- + +## 9. Source repositories + +- `disk-encryption-tool` - + https://github.com/openSUSE/disk-encryption-tool +- `jeos-firstboot` - https://github.com/openSUSE/jeos-firstboot +- `sdbootutil` - https://github.com/openSUSE/sdbootutil (see also its + own `ARCHITECTURE.md` for the bootloader-entry/snapshot side, not + duplicated here) +- `combustion` - https://github.com/openSUSE/combustion +- Background/narrative (predates the `systemd-pcrlock` migration + described here, which replaced the earlier `pcr-oracle`-based + signed-policy approach): + https://microos.opensuse.org/blog/2023-12-20-sdboot-fde/ ++++++ sdbootutil.obsinfo ++++++ --- /var/tmp/diff_new_pack.WDltGT/_old 2026-10-20 12:57:18.580195386 +0200 +++ /var/tmp/diff_new_pack.WDltGT/_new 2026-10-20 12:57:18.584195553 +0200 @@ -1,5 +1,5 @@ name: sdbootutil -version: 1+git20260929.26b6989 -mtime: 1790713266 -commit: 26b6989e346388bfa244b226c809a1e6a3824a0d +version: 1+git20261007.53bc127 +mtime: 1791375200 +commit: 53bc1271fcf2b544baafbc9bedd29d0494b9713a
