I use a pretty simple setup for booting my systems.
- The hardware firmware (UEFI) loads a signed bootloader (
systemd-boot
in my case, butgummiboot
is basically the same for non-systemd systems). - The bootloader loads a signed executable that bundles the kernel, initrd and the cmdline (I’ll call this the bundle" from here on).
- The
initrd
prompts for the encryption passphrase, mounted the decrypted disk, and then boots the actual OS.
That’s the basic boot process. Note that the bootloader is a single binary, and the bundle is another single binary. There are no moving parts, just both binaries are protecting from tampering by being signed. There are no extra modules, configuration files, or alike.
The OS itself has some extra steps before i see an actual desktop.
- Upon booting,
tty1
automatically logs into my user account and starts upsway
(the compositor). sway
itself starts all other user-session services (launcher, etc).
For the remainder of this artile, it is assumed that the EFI ESP partition is
mounted in /efi
. There is no /boot
partition.
systemd-boot
(reminder: gummiboot
is basically the same tool for non-systemd setups)
First of all, copy the bootloader into the EFI partition and register it with the UEFI firmware. Both things can be done with one command:
bootctl install
On startup, systemd-boot will automatically look for signed bundles in
EFI/Linux
. Since I usually have my primary bundle and a backup one, I
manually configure it to prefer the main one:
# The bundle is located in /efi/EFI/Linux/arch.efi:
bootctl set-default arch.efi
systemd-boot
requires no additional configuration.
I don’t configure a menu with a timeout since I want the system to boot directly without any prompt. In case I need to boot from the recovery one, the menu can be triggered by spamming Space during start-up.
Note that the menu will only allow picking another stub to boot from, but
overriding the cmdline
is not possible since it’s embedded in the bundle.
This prevents random strangers from executing arbitrary commands.
sbctl
sbctl
is a tool to manage SecureBoot keys and generate signed bundles with
the initrd
, cmdline
and kernel. Keeping all items in a single signed bundle
is important. As a counter-example: if the cmdline
is just a file in the same
partition, it can be altered, which would allow easily executing unsigned code.
The UEFI firmware first needs to be configured to use a key that will be
generated and managed by sbctl
.
Prepare the firmware
- Reboot into the firmware/UEFI menu.
- Disable Secure Boot.
- Turn on custom key mode (this implies “using user-provisioned keys”).
- Turn on setup mode (this implies “allow altering the user-provisinged keys”).
Set up the OS
- Reboot into ArchLinux (or your distro of choice).
- Create and enroll keys:
sudo sbctl create-keys
sudo sbctl enroll-key
Generate the bundles
sbctl
will read the cmdline from /etc/kernel/cmdline
, so make sure to put
your current cmdline there. This can be done with:
cat /proc/cmdline | sudo tee /etc/kernel/cmdline
My partitions has the right Partition Type
set, so systemd knows what
partition to pick up based on its id. This follows the Discoverable Partitions
Specification. The Partition Type can be safely
updated for partitions, even if currently mounted (since this updates the
GPT, not the partition itself).
Then, generate the sbctl
bundle:
sbctl bundle --sign /boot/EFI/Linux/arch.efi
The bundle will use the existing initrd
and kernel
. So each time
mkinitcpio
generates a new initrd
, the bundle needs to be rebuilt.
Fortunately, sbctl
includes pacman hooks to re-build the bundle when you
update the kernel or the initrd
.
The file /etc/kernel/cmdline
is read on each re-generation, so don’t delete
it. It’s also the file you want to update when you need to make changes to your
cmdline.
Finally, you should also sign the bootloader itself:
# This is the entry explicitly configured in the UEFI settings.
# The entry for it MAY be deleted during firmware updates.
sbctl sign --save /efi/EFI/systemd/systemd-bootx64.efi
# This is default fallback in case there are no entries in the UEFI.
# If you dualboot, some aggressive OSs may overwrite it:
sbctl sign --save /efi/EFI/BOOT/BOOTX64.EFI
Turning on SecureBoot
- Reboot into the firmware/UEFI menu.
- Enable Secure Boot (this implies “check that everything is signed”).
- Leave custom key mode on (e.g.: continue using the user-provisioned keys).
- Turn off setup mode (e.g.: disallow further altering the configured keys).
Reboot and cleanup
You should be able to reboot with SecureBoot turned on. The amount of moving parts in the whole boot process are extremely few, and it’s also very low-maintenance.
I’d recommend also removing all other unnecessary boot entries from the firmware using:
sudo efibootmgr --delete-bootnum --bootnum 00XX
If you’re unsure which one you should leave, delete all of them and re-run
bootctl install
afterwards.
Breakage recovery
This setup is unlikely to break by itself – but as with any Arch setup, it may break due to the user fiddling with things (e.g.: a broken cmdline).
I have a small tool that creates the above-mentioned backup bundle after every successful boot. If I break the main bundle and it doesn’t boot, then that image is kept around, and I can reboot off it.
As mentioned above, tapping Space during startup shows the boot entry menu.
Credit
sb-backup
relies on go-uefi
and sbctl
, so thanks to
the author for doing all the hard lifting.
Update 1
It seems that using SecureBoot with a discrete GPU can be problematic in some cases. Since the GPU’s firmware is not signed, it may fail to load in some cases due initialisation order, and may result in a bricked motherboard.
I’d recommend you do some serious research before trying this if you have a discrete GPU. See this reddit thread for more details.