feat(install): defer LUKS encryption to first boot#2096
feat(install): defer LUKS encryption to first boot#2096andrewdunndev wants to merge 1 commit intobootc-dev:mainfrom
Conversation
Replace the install-time cryptsetup/cryptenroll calls in the Tpm2Luks path with a first-boot encryption approach. At install time, the root filesystem is created 32MB smaller than the partition and a rd.bootc.luks.encrypt=tpm2 karg is added. On first boot, a dracut module runs cryptsetup reencrypt --encrypt to encrypt the root in-place, then enrolls TPM2 on real hardware with correct firmware state. This eliminates both the IPC namespace semaphore deadlock (bootc-dev#2089) and the shim/PCR mismatch problem (bootc-dev#421). Prior art: openSUSE disk-encryption-tool, which ships a production implementation of first-boot encryption using the same cryptsetup reencrypt --encrypt mechanism. Fixes: bootc-dev#2089 Related: bootc-dev#421, bootc-dev#476, bootc-dev#477 Signed-off-by: Andrew Dunn <andrew@dunn.dev> AI-Assisted: yes AI-Tools: GitLab Duo, OpenCode
There was a problem hiding this comment.
Code Review
This pull request introduces a new first-boot LUKS encryption mechanism for bootc. It defers the actual encryption and TPM2 enrollment of the root partition from installation time to the first boot, addressing issues with TPM2 enrollment in the install environment. This is achieved by reserving space for the LUKS header during installation, adding a new bootc-luks-firstboot.sh script and systemd service to the initrd, and updating the dracut module to include necessary components. Review comments highlight a critical flaw in the idempotency logic of the first-boot encryption script, which could leave the system unbootable if interrupted, and suggest a more robust method for parsing kernel command-line arguments.
| if ! should_encrypt; then | ||
| log "No encryption requested or already encrypted. Exiting." | ||
| exit 0 | ||
| fi | ||
|
|
||
| encrypt_root | ||
| configure_system |
There was a problem hiding this comment.
The script's idempotency logic is flawed. If the script is interrupted after encrypt_root but before configure_system completes, on the next boot should_encrypt will detect that the device is already a LUKS device and cause the script to exit. This will leave the system in an unbootable state because the bootloader configuration has not been updated to unlock the LUKS device.
The logic should be changed to ensure configure_system is always run if rd.bootc.luks.encrypt is present, even if the device is already encrypted. The should_encrypt function should be removed as its logic is now integrated into this main execution block.
| if ! should_encrypt; then | |
| log "No encryption requested or already encrypted. Exiting." | |
| exit 0 | |
| fi | |
| encrypt_root | |
| configure_system | |
| if [ -z "$ENCRYPT_KARG" ]; then | |
| log "No encryption requested. Exiting." | |
| exit 0 | |
| fi | |
| if [ -z "$ROOT_DEV" ]; then | |
| die "rd.bootc.luks.encrypt set but no root= device found" | |
| fi | |
| if ! cryptsetup isLuks "$ROOT_DEV" 2>/dev/null; then | |
| encrypt_root | |
| else | |
| log "Root device $ROOT_DEV is already LUKS. Skipping encryption." | |
| fi | |
| configure_system |
| local cmdline | ||
| cmdline=$(< /proc/cmdline) | ||
|
|
||
| for arg in $cmdline; do | ||
| case "$arg" in | ||
| rd.bootc.luks.encrypt=*) | ||
| ENCRYPT_KARG="${arg#rd.bootc.luks.encrypt=}" | ||
| ;; | ||
| root=UUID=*) | ||
| local uuid="${arg#root=UUID=}" | ||
| ROOT_DEV=$(blkid -U "$uuid" 2>/dev/null) || true | ||
| ;; | ||
| root=/dev/*) | ||
| ROOT_DEV="${arg#root=}" | ||
| ;; | ||
| esac | ||
| done |
There was a problem hiding this comment.
The current method of parsing /proc/cmdline using for arg in $cmdline relies on word splitting, which can fail if a kernel argument contains spaces. While it works for the current set of arguments, it's not robust. A safer approach is to read the command line into an array.
| local cmdline | |
| cmdline=$(< /proc/cmdline) | |
| for arg in $cmdline; do | |
| case "$arg" in | |
| rd.bootc.luks.encrypt=*) | |
| ENCRYPT_KARG="${arg#rd.bootc.luks.encrypt=}" | |
| ;; | |
| root=UUID=*) | |
| local uuid="${arg#root=UUID=}" | |
| ROOT_DEV=$(blkid -U "$uuid" 2>/dev/null) || true | |
| ;; | |
| root=/dev/*) | |
| ROOT_DEV="${arg#root=}" | |
| ;; | |
| esac | |
| done | |
| local arg | |
| local -a cmdline_args | |
| read -r -a cmdline_args < /proc/cmdline | |
| for arg in "${cmdline_args[@]}"; do | |
| case "$arg" in | |
| rd.bootc.luks.encrypt=*) | |
| ENCRYPT_KARG="${arg#rd.bootc.luks.encrypt=}" | |
| ;; | |
| root=UUID=*) | |
| local uuid="${arg#root=UUID=}" | |
| ROOT_DEV=$(blkid -U "$uuid" 2>/dev/null) || true | |
| ;; | |
| root=/dev/*) | |
| ROOT_DEV="${arg#root=}" | |
| ;; | |
| esac | |
| done |
Summary
Defer LUKS encryption to first boot instead of running cryptsetup inside the install container. This eliminates the IPC namespace semaphore deadlock (#2089) and the shim/PCR mismatch problem (#421) in one change, since TPM2 binding happens on real hardware with real firmware state.
Prior art: openSUSE disk-encryption-tool, which ships a production implementation of first-boot encryption using the same
cryptsetup reencrypt --encryptmechanism.Problem
bootc install to-disk --block-setup tpm2-luksrunscryptsetup luksFormat,systemd-cryptenroll, andcryptsetup luksOpeninside the install container. This has two problems:IPC namespace deadlock (install to-disk --block-setup tpm2-luks hangs: libdevmapper udev cookie semaphore deadlock in container IPC namespace #2089): libdevmapper uses SysV semaphores to coordinate with udevd. Inside a container with an isolated IPC namespace, udevd on the host cannot see the container's semaphores, causing
luksOpenandluksCloseto hang onsemop().Shim/PCR mismatch (install to-disk with LUKS + TPM broken #421): TPM2 enrollment during install binds to the container's firmware state, not the installed system's firmware. On first real boot, the PCR values differ and auto-unlock fails.
Approach
Install time: Write an unencrypted root partition with the filesystem created 32MB smaller than the partition. Add
rd.bootc.luks.encrypt=tpm2to the kernel command line. No cryptsetup calls, no devmapper, no TPM2.First boot: A dracut module (
51bootc) installs a systemd service that runs beforesysroot.mount. The service callscryptsetup reencrypt --encrypt --reduce-device-size 32Mto encrypt the root partition in-place using the reserved 32MB for the LUKS2 header. It then enrolls TPM2 viasystemd-cryptenroll, writes/etc/crypttab, and updates the BLS entry withrd.luks.uuid/rd.luks.namekargs.The
root=UUID=<ext4-uuid>karg does not change. Oncesystemd-cryptsetupunlocks LUKS on subsequent boots, the ext4 UUID inside becomes visible androot=resolves normally.Changes
crates/lib/src/install/baseline.rsTpm2Luksarm: remove all cryptsetup/cryptenroll callsmkfs_with_reserve()that creates the filesystem smaller than the partition (ext4: block count arg, XFS:-d size=, btrfs:-b)rd.bootc.luks.encrypt=tpm2karg instead ofluks.uuidluks_device = None(no luksClose needed)crates/initramfs/dracut/module-setup.shbootc-luks-firstboot.shand.serviceinto the initramfsdm_cryptkernel modulesysroot.mount.requiressymlink for service orderingcrates/initramfs/luks-firstboot/bootc-luks-firstboot.sh(new)rd.bootc.luks.encryptkarg from/proc/cmdlinecryptsetup isLuksbefore encryptingcryptsetup reencrypt --encrypt --reduce-device-size 32Msystemd-cryptenroll --tpm2-device=autosystemd-cryptenroll --recovery-key/etc/crypttaband BLS entriescrates/initramfs/bootc-luks-firstboot.service(new)Before=sysroot.mountin the initrdConditionKernelCommandLine=rd.bootc.luks.encryptOnFailure=emergency.target(drops to shell on failure)Makefile/usr/lib/bootc/Testing
Full end-to-end validation on GCP n2-standard-8 with nested KVM, Fedora 42 (cryptsetup 2.8.4, systemd 257.11, QEMU 9.2.4 + OVMF + swtpm).
Build verification
cargo checkcargo build --releasecargo clippy -p bootc-libInstall verification
Installed with patched bootc binary via
bootc install to-disk --block-setup tpm2-luks --filesystem ext4to a 20GB disk.rd.bootc.luks.encrypt=tpm2in BLS entryEncryption verification
Manually encrypted the installed root partition using the same
cryptsetup reencrypt --encrypt --reduce-device-size 32Mcommand the first-boot script uses.e2fsckclean after encryptioncrypttabwritten to ostree deployrd.luks.uuidBoot verification
Booted the encrypted system in QEMU with swtpm (vTPM 2.0). The initramfs was patched to include the first-boot dracut module with
systemd-cryptsetupsupport.systemd-cryptsetup@cr_root.servicestarted/dev/mapper/cr_rootdevice createdsysroot.mountsucceeded (ostree root)Serial console output (key lines):
Encryption mechanism validation (independent)
Tested
cryptsetup reencrypt --encrypt --reduce-device-size 32Mindependently across multiple filesystem types and sizes.Additional mechanism tests:
cryptsetup isLuksdetects existing LUKS,reencrypt --encryptrejects already-encrypted devices ("Device is already LUKS device. Aborting.")Not tested (requires project CI)
Fixes: #2089
Related: #421, #476, #477
Signed-off-by: Andrew Dunn andrew@dunn.dev
AI-Assisted: yes
AI-Tools: GitLab Duo, OpenCode
AI-Generated Content Disclosure: This PR contains code generated with assistance from GitLab Duo and OpenCode. The output has been reviewed for correctness, tested, and validated against project requirements per GitLab's AI contribution guidelines.