js' blog

Actually secure boot on Fedora
Created: 12.08.2021 20:11 UTC

A few months ago I purchased a new notebook which supports Secure Boot. It is actually my very first machine with Secure Boot support on which I want to run Linux (the only other machine I have which supports Secure Boot is my gaming rig, which exclusively runs Windows as it has no other purpose than playing games).

While at first I was happy to finally have a somewhat secure boot chain, the disappointment came quickly: Pretty much all Linux distributions which support Secure Boot use a shim called shim which is signed with a key from Microsoft. shim then checks the signature of the kernel against certificates stored in an UEFI variable and failing that falls back to checking against the Secure Boot keys stored in UEFI. While I do believe that UEFI somewhat protects its own certificate store, I am not so sure about that of shim.

But this is not the only problem with the default Secure Boot setup on Fedora and most other distributions: The initrd is not verified at all! Yes, that's right - someone can just put a malicious initrd on your unencrypted /boot partition and there is absolutely nothing that prevents it from being loaded! The kernel and GRUB (at least by default) only implement the bare minimum for Secure Boot: Making sure that no unsigned code runs in kernel space while entirely ignoring everything that would compromize user space. That makes Secure Boot entirely useless by default, as compromizing the initrd is even much easier than comprimizing the kernel: Just swap the /sbin/init in the initrd with a shell script that opens a backdoor and then calls the real init instead of cumbersomely inserting a backdoor into the kernel.

So, is Secure Boot entirely useless? No! It is just that the way all Linux distributions use it by default is entirely useless, because their goal is to get everything signed by Microsoft so that users can boot the system without needing to reconfigure Secure Boot. Their goal lis not to be secure. But we can fix all of that! The idea is to combine the kernel, initrd and boot options into a single .efi file that we sign with our own key.

So first of all, we need to switch from GRUB to systemd-boot. But wait! Don't go follow any tutorial on how to do that - because we're doing things a bit differently. So why do we need to switch? GRUB is very hard to secure. It reads many unverified files and its configuration is by default entirely unverified. It also by default does not verify the initrd. With a lot of work and assembling your own GRUB, it is possible - but this is very error prone, fragile and GRUB recently changed things around so that all the instructions that you will find on how to let GRUB verify everything are outdated. systemd-boot on the other hand is simple: You take the systemd-boot stub, the kernel, the initrd and the boot options and merge it into a single .efi file that you then sign.

Now is a good time to have a Fedora DVD ready to boot in case you break something! Do not proceed if you have no medium to boot from for recovery!

To uninstall GRUB, do:

sudo rm /etc/dnf/protected.d/shim.conf
sudo dnf remove shim\* memtest86+

Do not reboot now! Your machine is in an unbootable state now!

Time to save the kernel boot options:

cat /proc/cmdline | cut -d ' ' -f 2- | sudo tee /etc/kernel/cmdline

Next, we need to generate a few keys. But first, let's look at what keys Secure Boot has:

This means you can import a DB or DBX from a booted system if it is signed with a KEK, and you can import a KEK from a booted system if it is signed with the PK. If you import it directly in the firmware (we will do this later), no such signature is needed. But you still need a PK and KEK, as otherwise Secure Boot will remain in setup mode - meaning the OS can import keyes without requiring a signature.

We need to generate our own PK, KEK and DB. To generate those keys, we do the following:

sudo dnf install openssl
sudo mkdir /etc/secureboot
sudo chmod 700 /etc/secureboot
sudo openssl req -new -x509 -newkey rsa:4096 -subj "/CN=$(hostname) PK/" -days 36500 -nodes -sha256 -keyout /etc/secureboot/pk.key -out /etc/secureboot/pk.crt
sudo openssl req -new -x509 -newkey rsa:4096 -subj "/CN=$(hostname) KEK/" -days 36500 -nodes -sha256 -keyout /etc/secureboot/kek.key -out /etc/secureboot/kek.crt
sudo openssl req -new -x509 -newkey rsa:4096 -subj "/CN=$(hostname) DB/" -days 36500 -nodes -sha256 -keyout /etc/secureboot/db.key -out /etc/secureboot/db.crt
sudo openssl x509 -in /etc/secureboot/pk.crt -outform DER -out /etc/secureboot/pk.cer
sudo openssl x509 -in /etc/secureboot/kek.crt -outform DER -out /etc/secureboot/kek.cer
sudo openssl x509 -in /etc/secureboot/db.crt -outform DER -out /etc/secureboot/db.cer
sudo sh -c 'cp /etc/secureboot/*.cer /boot/efi'

We won't need the unencrypted /boot anymore, so let's move /boot to the encrypted root partition so that nobody can swap the kernel or initrd on the unencrypted /boot partition and trick us into signing it later.

sudo umount /boot/efi
sudo cp -a /boot /bootx
sudo umount /boot
sudo rmdir /boot
sudo mv /bootx /boot
sudo mount /boot/efi
sudo -e /etc/fstab  # Remove /boot from it, but not /boot/efi

In order to automatically merge systemd-boot, the kernel, initrd and the boot options into a a single .efi file and sign it when a new kernel is installed, I wrote a kernel-install hook. All you need to do is drop 100-combine-and-sign.install into /etc/kernel/install.d and make it executable. After adding this file, do the following and you should end up with everything as a single, signed .efi file:

sudo dnf install systemd-boot sbsigntools binutils
sudo mkdir -p /boot/efi/EFI/Linux
sudo dnf reinstall kernel-core-$(uname -r)

That's it from the Linux side. Now it's time to go into your UEFI settings: Change the file to boot to EFI/Linux/linux.efi. Then clear all existing keys (PK, KEK and DB - you can also clear DBX, as it is now useless: it only contains hashes you could not boot anyway) and import the PK, KEK and DB that we copied to the EFI partition (you can and should delete those from the EFI partition after importing them). And obviously, set a UEFI password (but if you read this guide, you probably are the kind of person who already has that anyway, right?). If everything worked, your system should come up again now. Good luck!

If you like to clean up, you can actually get rid of all files in /boot/efi except for /boot/efi/EFI/Linux/linux.efi. Use dnf provides /boot/efi/foo to see which package added the unnecessary files and uninstall them.

Edit: Please see here if you are running Fedora 39, as things changed in Fedora 39, requiring changes to this process.