June 27, 2026 (System76, Dasharo/coreboot+UEFI v1.0.1) · Ubuntu 26.04 LTS, kernel 7.0.0-27-generic
ubuntuukisystemd-bootlukstpm2secure-bootdasharomeasured-bootcoreboot
I spent a day migrating my encrypted Ubuntu install from shim/GRUB to a measured Unified Kernel Image, getting TPM auto-unlock working again, enabling Secure Boot without bricking the machine, and learning - repeatedly - that rebuilding the UKI without re-enrolling the TPM is a great way to lock yourself out of your own disk at boot.
- Why I did this
- The target architecture
- Phase 1: UKI migration and the first TPM mismatch
- Phase 2: The crypttab lie
- Phase 3: The UKI rebuild trap
- Phase 4: The boot menu that was not LUKS
- Phase 5: Secure Boot - and how I bricked myself once
- Phase 6: Plymouth, scripting hell, and passphrase rotation
- Side quests that still mattered
- What I would tell past-me
- Final state
- What I automated
- Closing thoughts
I wanted a modern Linux boot stack: systemd-boot, a Unified Kernel Image (UKI), measured boot via the TPM, LUKS auto-unlock sealed to PCR measurements, and eventually Secure Boot with my own signing keys.
The old path - shim → GRUB → separate kernel and initrd - worked fine for years. But it does not participate cleanly in systemd's measured-UKI world, where the bootloader stub measures the kernel, command line, and initrd as a single signed bundle. I also wanted LUKS to unlock automatically at boot without typing a passphrase every time, while still keeping full-disk encryption.
What I did not anticipate was how many subtle coupling points exist between UKI hashes, PCR 7, crypttab, initrd contents, and TPM enrollment - and how easy it is to fix one layer while silently breaking the one below it.
When everything is working, the boot chain looks like this:
- Firmware (Dasharo UEFI) hands off to Linux Boot Manager (
Boot0002- systemd-boot at/EFI/systemd/systemd-bootx64.efi). - systemd-boot loads a Type #2 UKI from
/EFI/Linux/*.efi- kernel, initrd, cmdline, and PCR signatures bundled together. - systemd-stub measures the UKI and extends TPM PCRs before the kernel runs.
- The initrd runs
systemd-cryptsetup, which tries to unlock LUKS using a TPM2 token sealed to a pcrlock policy (/var/lib/systemd/pcrlock.json). - If the PCR measurements match, the disk unlocks in about two seconds. No prompt. I go straight to the desktop.
Final loader.conf:
timeout 0
console-mode keep
default a2e021c04bd8456a8cdf62ef71327e67-*
auto-firmware no
editor noFinal /etc/crypttab:
dm_crypt-0 UUID=2090256b-18b4-4e38-b2bc-31d3def623e4 none tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,luksI ran a migration procedure that installed kernel/UKI configuration (layout=uki, ukify as the generator), generated PCR signing keys for measured boot, built the first UKI with kernel-install, installed systemd-boot as the primary EFI entry, and set loader.conf for instant boot with no firmware-menu entry. The first boot into measured UKI was exciting - bootctl status reported Measured UKI: yes, and I could see the initrd PCR barrier firing:
systemd-pcrphase-initrd.service started
Extended PCR index 11 with 'enter-initrd'
Extended PCR index 11 with 'leave-initrd'
But LUKS did not auto-unlock. Twice I saw:
TPM policy does not match current system state... Operation not permitted
Then the initrd fell back to a passphrase prompt on /dev/tty1.
This was partially expected. My old TPM binding was from the pre-UKI world. The measurements had changed. After I reached multi-user, a one-shot systemd service I had installed during migration ran successfully on first boot: it called systemd-pcrlock make-policy, wiped the old TPM slot, enrolled a new token in keyslot 1, rebuilt the UKI, and created a marker file so it would not run again.
I rebooted, confident it would work.
It did not.
The second boot still demanded my passphrase. Same TPM policy mismatch. The enrollment service correctly skipped (marker file present), so nothing re-ran to fix it.
I dug into journals and found the real bug: my crypttab and my TPM enrollment disagreed about which PCR policy to use.
/etc/crypttab still said:
tpm2-pcrs=11+12But systemd-cryptenroll had automatically enrolled against the pcrlock policy at /var/lib/systemd/pcrlock.json, which on my Dasharo firmware locks PCRs 3, 7, 13, and 14 - not 11+12.
Why? The first-boot enrollment helper looked for the pcrlock policy at /run/systemd/tpm2-pcrlock-policy.json, but systemd-pcrlock make-policy had written it to /var/lib/systemd/pcrlock.json. The helper fell through to a legacy tpm2-pcrs=11+12 crypttab branch even though enrollment had already chosen pcrlock.
So initrd systemd-cryptsetup was asking the TPM to validate one policy while the LUKS token was sealed to another. No wonder it kept saying "operation not permitted."
I ran a repair procedure that:
- Updated
/etc/crypttabtotpm2-pcrlock=/var/lib/systemd/pcrlock.json - Added a dracut snippet so the initrd embeds
/var/lib/systemd/pcrlock.json - Rebuilt the UKI with
kernel-install - Fixed the first-boot enrollment helper to resolve the pcrlock path from either the runtime symlink or the persistent file
I rebooted twice more. Still passphrase. The crypttab mismatch was real and necessary to fix - but it was not the whole story.
Here is the lesson that cost me the most time:
Any UKI rebuild changes PCR 7 (secure-boot-policy), which invalidates the TPM seal - unless you re-enroll in the same atomic step.
That repair procedure rebuilt the UKI at 07:00. My TPM token and pcrlock policy were from 06:50. Ten minutes apart. Different UKI hash. Dead seal.
The first-boot enrollment service would not run again because its .done marker existed. I was stuck in a loop: correct crypttab, correct initrd, wrong seal.
The fix is always the same three-step atomic sequence:
systemd-pcrlock make-policy
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrlock=/var/lib/systemd/pcrlock.json <device>
kernel-install add "$(uname -r)" "/boot/vmlinuz-$(uname -r)"Do those together. Then reboot. Never rebuild the UKI "just because" without re-enrolling.
Manual re-enrollment around 07:20 finally worked. Boot at 07:21:17 - clean two-second cryptsetup, no prompt. I could breathe again.
With TPM unlock working, I hit another annoyance: a five-second systemd-boot menu listing my UKI entry and "Reboot Into Firmware Interface." Keyboard input did not work reliably. I initially thought it was a LUKS recovery prompt.
It was not. It was timeout 5 plus auto-firmware defaults left over from migration prep. Dasharo already has its own firmware recovery UI (Boot0001), so this menu was redundant.
I set timeout 0, auto-firmware no, and editor no in loader.conf, ran bootctl set-timeout 0, and updated the migration procedure so re-running it would not bring the menu back. Instant boot restored.
I wanted Secure Boot with custom keys, not Microsoft's. I generated a keypair, signed systemd-boot and the UKI, and placed auto-enrollment .auth files on the ESP for systemd-boot's secure-boot-enroll force feature.
I got the order wrong once and paid for it.
I enabled Secure Boot in Dasharo firmware before the custom keys were enrolled. On the next boot:
Secure Boot is enabled.
The image signature is invalid or missing!
Booting 'Linux Boot Manager' failed due to 'Access Denied'.
Firmware verifies signatures before any bootloader runs. With SB on but my custom db key not yet in firmware, systemd-boot was rejected immediately. The auto-enrollment code never executed. timeout 0 meant I had no pause to hit F2. Boot0003 (old Ubuntu/shim) was also untrusted.
I had to physically remove the boot drive to get into Dasharo Setup and disable Secure Boot.
- Enter Setup Mode - on Dasharo, "Erase to default Secure Boot Keys" (not Reset), then Reset. Confirm
SetupMode = 1. - Boot normally - systemd-boot shows a 15-second enrollment countdown and installs PK/KEK/db from the ESP. Auto-reboot.
- Only then enable Secure Boot in firmware.
- First SB-on boot may need a passphrase once; a one-shot service I had installed for post-SB rollout re-binds TPM to the new PCR 7 state (refresh pcrlock, wipe and re-enroll the TPM slot, update crypttab, rebuild UKI).
- Reboot again to confirm auto-unlock.
- Restore instant boot: set
loader.confback totimeout 0, remove thesecure-boot-enrolllines, and put Linux Boot Manager first in EFI boot order.
My first Phase A attempt failed silently because SetupMode was still 0 - default Microsoft keys were present from an earlier failed attempt. No countdown, no enrollment. I had to erase keys properly first.
plymouth-start.service was segfaulting, so I removed Plymouth entirely: purged packages, dropped splash from the kernel cmdline, rebuilt the UKI. Cleaner boot, easier debugging.
Side effect: UKI rebuild → PCR 7 change → TPM seal broken. Same trap as before.
I spent some time trying to non-interactively feed my LUKS passphrase to systemd-cryptenroll. What failed:
| Approach | Result |
|---|---|
printf pass | sudo -A systemd-cryptenroll |
sudo -A steals stdin for askpass |
printf pass | sudo systemd-cryptenroll with TTY |
cryptenroll uses ask-password socket, ignores stdin |
$PASSWORD with trailing newline from printf '%s\n' |
Wrong bytes - passphrase rejected |
| zenity / nested sudo bash | Broken capture in script |
What worked:
$PASSWORDenvironment variable - supported but poorly documented (systemd#20955)- Exact bytes, no trailing newline:
PASSWORD=$(printf '%s' '...') - Atomic sequence: pcrlock → cryptenroll → kernel-install in one session
I also learned that sudo askpass and LUKS passphrase are different prompts - easy to confuse when debugging.
My boot passphrase worked fine (slot 0); the automation was corrupting it. I wrote a small diagnostic that prompts for the boot secret and tests it against keyslots 0 (password) and 2 (recovery) with cryptsetup open --test-passphrase - slot 0 matched, slot 2 did not. Interactive systemd-cryptenroll on a real TTY succeeded immediately.
After everything was stable with Secure Boot on (verified boot 09:46:38), I rotated my LUKS passphrase. Boot 10:08:47 required the new passphrase - expected - but TPM auto-unlock was gone entirely. Rotation had replaced keyslot 0 and removed the TPM token without re-enrollment.
Re-enrolled at 10:18. Boot 10:20:48 - success.
Then the post-Secure Boot one-shot enrollment service fired because its marker file was missing, wiped keyslot 1, and hung waiting for a passphrase in a non-interactive context. I stopped the service, recreated the marker, and ran manual re-enrollment again - the same atomic three-step sequence: refresh pcrlock policy, enroll TPM2 token against it, rebuild UKI.
Boot 10:26:51 - TPM auto-unlock working. The post-SB service correctly skipped. Stable.
/boot/efi is vfat - chmod does nothing useful. I fixed bootctl's random-seed security warning by mounting the ESP with fmask=0177,dmask=0077 in fstab so files are 0600 and directories 0700 from Linux's perspective.
systemd-pcrlock dropped several PCRs (0, 1, 2, 4, 5, 11, 15) due to unrecognized VBOOT/FMAP measurements in the TPM event log. It still locked 3, 7, 13, 14. Normal on coreboot - not a sign of failure.
A stale fstab entry for my OWC external RAID0 (517bd6c2-…) caused a 10-second wait when the drive was not connected. I set noauto in fstab and added a udev rule for hot-plug mount/unmount at /mnt/owc-raid0. Boot got noticeably snappier.
- Treat UKI rebuild and TPM enrollment as one operation. If the UKI hash changes, re-enroll. Full stop.
- Crypttab must match enrollment. If you use pcrlock, say so explicitly:
tpm2-pcrlock=/var/lib/systemd/pcrlock.json. Embed the policy in the initrd. - Secure Boot key enrollment happens while SB is off or in Setup Mode. Enroll keys first, enable SB second. Reversing this bricks boot.
- Dasharo Setup Mode requires erasing keys, not resetting to defaults. Check
SetupModebefore expecting the 15-second countdown. - Passphrase rotation removes TPM tokens. Re-run enrollment after changing the LUKS password.
- Watch for one-shot services with stale markers. A service that wipes your TPM slot on boot is worse than no service at all.
systemd-cryptenrollwants exact passphrase bytes. No newline.$PASSWORDworks; pipes usually do not.- Not every boot prompt is LUKS. systemd-boot menus and firmware recovery UIs look scary but are a different problem.
As of boot 10:26:51 on June 27, 2026:
| Item | Status |
|---|---|
| Boot loader | systemd-boot 259.5 (Boot0002 - Linux Boot Manager) |
| UKI | Measured, signed |
| Secure Boot | Enabled (mokutil --sb-state) |
| LUKS auto-unlock | TPM2 via pcrlock, ~2 seconds, no prompt |
| LUKS slots | 0 = password, 1 = tpm2, 2 = recovery |
| Boot timeout | 0 (instant boot, no menu) |
| Plymouth | Removed |
| ESP | Hardened mount options |
| External RAID | Hot-plug only, no boot wait |
Verification commands I still run after any boot-related change:
journalctl -b -0 -u 'systemd-cryptsetup@*'
mokutil --sb-state
bootctl status
sudo cryptsetup luksDump /dev/nvme0n1p3 | grep -A1 systemd-tpm2A clean cryptsetup journal looks like this - nothing else:
Starting systemd-cryptsetup@dm_crypt-0.service
Finished systemd-cryptsetup@dm_crypt-0.service
By the end of the day I had wrapped the repeated manual steps into a few procedures. None of them are generic drop-in tools for every machine - they encode assumptions about my disk UUID, machine ID, and Dasharo firmware - but the patterns transfer.
Installed /etc/kernel/install.conf, cmdline, and uki.conf; generated PCR signing keys; built the UKI with kernel-install; put systemd-boot first in EFI boot order; configured instant boot in loader.conf; and installed a one-shot systemd service that runs after the first successful measured-UKI boot to re-bind TPM unlock.
Rewrote /etc/crypttab to use tpm2-pcrlock instead of legacy tpm2-pcrs, added a dracut config to embed pcrlock.json in the initrd, rebuilt the UKI, and fixed the first-boot enrollment helper's pcrlock path lookup.
What I ran many times by hand. Executes the atomic sequence: lock firmware/secure-boot PCRs, run systemd-pcrlock make-policy, wipe any existing TPM2 LUKS token, enroll a new one with systemd-cryptenroll --tpm2-pcrlock, then rebuild the UKI. The version that finally worked prompts for the LUKS unlock secret on a real TTY rather than trying to pipe or env-var it through nested sudo.
A short interactive check that takes the exact secret typed at boot and tests it against keyslots 0 and 2, to distinguish "wrong passphrase" from "script corrupted the passphrase bytes."
Generated a custom SB keypair, signed systemd-boot and the UKI, populated /loader/keys/auto/*.auth on the ESP, temporarily set loader.conf to timeout 5 with secure-boot-enroll force and a 15-second countdown, and installed a second one-shot service for TPM re-bind after SB is enabled.
Verified mokutil --sb-state reports enabled, then restored timeout 0, stripped enrollment directives from loader.conf, and reset EFI boot order.
Runs once when measured UKI is active and no marker file exists: refresh pcrlock, wipe TPM slot, enroll new token, write matching crypttab, rebuild UKI, touch marker.
Runs once when Secure Boot and measured UKI are both active: same flow, but also locks secure-boot-authority PCRs before making policy - because enabling SB changes PCR 7.
Modern Linux boot on UEFI is not one configuration file. It is a chain of sealed measurements: firmware trusts the bootloader, the bootloader measures the UKI, the UKI carries an initrd that must know how to talk to the TPM the same way the enrollment tool did when it sealed the key.
I migrated for security and simplicity. I got both - eventually - but only after learning that "it booted" and "it booted correctly" are very different statements when a TPM is involved.
If you are on a similar path: read the journals, keep enrollment atomic, and when something works, resist the urge to rebuild the UKI "just to clean things up" unless you are ready to re-enroll and reboot immediately after.
My machine now boots in silence. No menu. No passphrase. Secure Boot on. Measured UKI yes. That is worth the trouble.
I kept a separate technical session log with boot-by-boot timestamps, journal excerpts, and verification tables - useful when debugging, but not the kind of thing you publish as a blog post.