Generating a unified kernel image with minimal modification to distribution defaults
The aim of this process is to improve on the out-of-the-box security and convenience that a default full disk encryption (FDE) setup of most modern linux distribution offers. The target systems are laptops with UEFI and TPM 2.0 chips.
The overarching inspiration for writing this up can be found in Lennart Poetterings blog post from 2022.
Most of these steps can easily be adapted to any distribution that ships with systemd 256 or newer. Alternatives exist for other distributions and init systemd, but requires a bit more tweaking.
The goals are:
- Leave the default installation "as is": For Fedora 41, this means GRUB as bootloader, unencrypted /boot partition and LUKS-encrypted /root (and /home, /var etc., if set up)
- Use shim as provided in the default installation. The reason for this is that shim allows adding custom MOKs without bothering with replacing PK, KEK and db, which can lead to bricking modern laptops. (If you're comfortable enrolling custom keys on your hardware, it is advisable to include microsoft's key and/or built in firmware keys to ensure any OptionROM continues to function). In any case, the following method doesn't require that.
- Use UKI based on newest kernel by default, let TPM unlock LUKS partition.
- Allow distribution to update GRUB, shim, dracut, microcode and firmware updates as normal. If the UKI generation fails at some future point, you can still boot your computer through GRUB entries as normal.
Most online guides will suggest you install systemd-boot, refind or similar, or that you manually add efi boot entries for your UKI. Although those all work, it creates maintenance overhead because you need to keep those efi binaries up to date manually, maintain copies of shim etc. So, let's avoid that.
Very first step: Set a good multi word password for your UEFI BIOS to prevent any unauthorized changes to firmware.
Prepare Dracut and initramfs
The first order of business is getting Dracut ready to unseal TPM for LUKS decryption to work.
Use nano to add the contents of crypt-tpm-pcr.conf file in /etc/dracut.conf.d/
add_dracutmodules+=" crypt systemd-pcrphase tpm2-tss "
Create the command line parameters to include in your UKI:
sudo cat /proc/cmdline > /etc/kernel/cmdline
Edit the file as required. Usually it is sufficient to indicate the root file system. I also disable the dracut rescue shell to prevent an attacker from unlocking a different encrypted partition in order to extract TPM-values.
NOTE: When reviewing this guide, it's become clear that you should not include any "rd.luks" entries on the command line, your initrd should be able to figure this out from /etc/crypttab (see below). You should also indicate the root file system by explicitly referring to the /dev/mapper mountpoint name for the LUKS partition. You can find this by typing mount in a terminal and check the mountpoint for root (/).
e.g.: root=/dev/mapper/luks-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx ro rhgb quiet rd.shell=0 rd.emergency=reboot
Edit crypttab and indicate that you want the TPM to extend the volume key for the LUKS partition (into PCR 15):
sudo nano /etc/crypttab
It should look something like this; you only need to edit the fourth field:
luks-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx none discard,tpm2-measure-pcr=yes,tpm2-device=auto
Install software and generate necessary keys
Install necessary software to allow generating keys, UKI, provide code signing etc. (note, we only need systemd-boot-unsigned in order for ukify script to find and incorporate sd-stub. This happens automatically in the background when ukify runs later on).
sudo dnf install systemd-ukify systemd-boot-unsigned tpm2-tools sbsigntools openssl mokutil
Create a directory to hold your configuration files, and enter it
sudo mkdir /etc/secboot && cd /etc/secboot
Generate a MOK used for signing your UKI
sudo openssl req -x509 -nodes -new -sha256 -days 3650 -newkey rsa:4096 -subj '/CN=SB MOK/' -keyout mok.key -out mok.crt && sudo openssl x509 -outform DER -in mok.crt -out mok.cer
Enroll the generated key with mokutil (and choose some password to enter upon reboot when asked to enroll)
sudo mokutil --import mok.cer
Reboot the computer, continue in MokManager to enroll and type in password from last step. Reboot again.
Reenter the same directory and generate keys for TPM-unlocking
cd /etc/secboot
sudo ukify genkey --pcr-private-key=/etc/secboot/pcr-initrd.key.pem --pcr-public-key=/etc/secboot/pcr-initrd.pub.pem
sudo ukify genkey -pcr-private-key=/etc/secboot/pcr-system.key.pem --pcr-public-key=/etc/secboot/pcr-system.pub.pem
Set up automatic UKI generation and booting
Create the script that generates the UKI with nanoor similar, paste the contents of generate-uki.sh
Make the file executable:
sudo chmod 700 /etc/secboot/generate-uki.sh
Create the UKI configuration with nano or similar, paste the contents of uki.conf
Then create the directory where the UKI should be stored, and link to the above script file so that it runs automatically on kernel upgrades:
sudo mkdir -p /boot/efi/EFI/Linux && sudo mkdir /etc/kernel/postinst.d && sudo ln -s /etc/secboot/generate-uki.sh /etc/kernel/postinst.d/generate-uki
Then create a custom grub entry so that the UKI can be chainloaded. Use nano or similar, put the contents of custom.cfg in /boot/grub2, edit the fs-uuid to reflect your EFI partition.
Set grub to load the UKI by default by modifying /etc/default/grub and add/modify the following line
GRUB_DEFAULT="uki"
Update grub with sudo grub-mkconfig -o /boot/grub2/grub.cfg
Finally, regenerate the initramfs with sudo dracut --regenerate-all --force
You can now manually generate a UKI for the running kernel by executing kernel-install add $(uname -r) /boot/vmlinuz-$(uname -r)
Reboot, you should now be booting your UKI :)
Almost done, now just seal the TPM policy
This will let the TPM unlock your LUKS partition as long as secure boot is active and no other MOKs have been added. PCR11 is extended by systemd and ensures the integrity of the UKI and initrd extends the luks header in PCR15. Adjust the partition name if necessary.
sudo systemd-cryptenroll --wipe-slot=tpm2 --tpm2-public-key=/etc/secboot/pcr-initrd.pub.pem --tpm2-pcrs=7+14+15:sha256=0000000000000000000000000000000000000000000000000000000000000000 --tpm2-device=auto /dev/nvme0n1p3
On next reboot, LUKS partition should automatically unlock. It should continue to do so after kernel upgrades etc.
Note: If you want to use third party kernel modules, the easiest solution is to enroll the MOK that DKMS automatically creates: sudo mokutil --import /var/lib/dkms/mok.pub, this will invalidate PCR 14, so just re-run the above systemd-cryptenroll command afterwards.
Update: After consulting the comment left by slewsys below (twice), I've updated the guide slightly, to include cryptenroll binding to an empty PCR15. The effect will be that after your initrd has unlocked the LUKS partition, PCR15 is extended to a non-zero value (no need to edit /etc/crypttab, setup in this quide does that automatically) and it will not be possible to retrieve the LUKS key from the TPM. In case an attacker has added a rogue LUKS partition, retrieving the key will thus not work. You can verify everything works by checking the TPM-log: sudo cat /run/log/systemd/tpm2-measure.log . Verify that PCR15 contains measurement by cryptsetup ("eventType":"volume-key")
Very nice, thank you! It might be worth adding that if the drive is moved to another system, the original LUKS passphrase is preserved and can be used to access it, e.g.:
And subsequently: