Arch Linux Secure Boot with systemd-boot, sbctl and a Unified Kernel Image

AI-generated

I recently wanted to enable Secure Boot on an Arch laptop without adding shim or a Microsoft signed boot chain. Using my own keys with sbctl and a Unified Kernel Image is a quite clean way to do this.

A UKI bundles the kernel, initramfs, kernel command line and systemd stub into one signed EFI file. systemd-boot can discover this file directly, so there is no separate boot entry and kernel image to keep in sync.

Before starting

I am assuming an UEFI Arch installation with the EFI System Partition mounted at /efi. /boot can be a normal Linux filesystem in this setup, as the UKI itself is written to the ESP.

First check the current mount points:

$ findmnt /efi
TARGET SOURCE         FSTYPE OPTIONS
/efi   /dev/nvme0n1p1 vfat   rw,relatime,...

I would also make sure the system boots normally with systemd-boot before changing Secure Boot keys. On my Arch machines I install it with:

# bootctl install
Created "/efi/EFI/systemd".
Created "/efi/EFI/BOOT".
Created EFI boot entry "Linux Boot Manager".

Keep a bootable Arch USB stick nearby. Secure Boot setup is usually simple, but a wrong path or an unsigned bootloader can leave you at a firmware error screen.

If you are using a laptop with firmware updates from Linux, make sure the firmware itself is current first. I previously wrote about doing a ThinkPad P1 UEFI BIOS update on Linux. Firmware updates can reset Secure Boot settings or change boot order.

Installing sbctl and creating keys

Install the required packages:

# pacman -Syu sbctl systemd mkinitcpio

sbctl stores its own Secure Boot keys below /var/lib/sbctl/keys. These are private keys, so I include this directory in my encrypted backup. Do not copy it to an unencrypted ESP or put it into a dotfiles repository.

Check the current firmware state:

# sbctl status
Installed:      ✘
Setup Mode:     ✘
Secure Boot:    ✘
Vendor Keys:    microsoft

To enroll own keys, the firmware needs to be in Setup Mode. Most UEFI setups have an option named Clear Secure Boot Keys, Delete All Secure Boot Variables, or similar. Disable Secure Boot first, clear the existing keys, save and boot Arch again.

After clearing the keys, check again:

# sbctl status
Installed:      ✘
Setup Mode:     ✔
Secure Boot:    ✘
Vendor Keys:    none

Now create my Platform Key, Key Exchange Key and signing keys:

# sbctl create-keys
Created Owner UUID 12345678-1234-1234-1234-123456789abc
Creating secure boot keys... done

Then enroll them:

# sbctl enroll-keys -m
Enrolling keys to EFI variables... done

The -m option is important for many machines. It additionally enrolls Microsoft's certificates into the Secure Boot database.

This does not mean that Arch itself is signed by Microsoft. My sbctl key still signs the systemd bootloader and UKI. The Microsoft keys are useful when firmware needs to load a Microsoft-signed Option ROM, when dual booting Windows, or when booting some external media.

This can matter with dedicated GPUs and PCIe cards. On machines where I use graphics hardware for KVM GPU pass-through, I would not remove Microsoft keys blindly. Some firmware validates GPU Option ROMs before the Linux bootloader even starts.

If you want a fully private trust store, leave out -m. Test this carefully before enforcing Secure Boot. A missing Microsoft certificate can cause Windows, third party boot media or hardware Option ROMs to stop working.

Building a Unified Kernel Image with mkinitcpio

Arch's current mkinitcpio can build UKIs directly. I use /etc/kernel/cmdline for the kernel command line embedded into the image.

For a normal root filesystem, get its UUID first:

$ findmnt -no UUID -T /
f0b1c2d3-1234-4567-89ab-cdef01234567

Create /etc/kernel/cmdline with your own root UUID:

# vim /etc/kernel/cmdline
root=UUID=f0b1c2d3-1234-4567-89ab-cdef01234567 rw

For LUKS, Btrfs subvolumes or LVM, this command line needs the same additional parameters that your current boot entry uses. Do not just copy the simple example above if your installation needs rd.luks.name, rootflags=subvol= or similar options.

Next I edit the Linux preset:

# vim /etc/mkinitcpio.d/linux.preset

Add a UKI path for the default preset:

default_uki="/efi/EFI/Linux/arch-linux.efi"

I also create a fallback UKI. This is optional, but useful while changing kernel hooks or storage configuration:

fallback_uki="/efi/EFI/Linux/arch-linux-fallback.efi"

The relevant part of the preset then looks similar to this:

PRESETS=('default' 'fallback')

default_uki="/efi/EFI/Linux/arch-linux.efi"
fallback_uki="/efi/EFI/Linux/arch-linux-fallback.efi"

Build all configured images:

# mkinitcpio -P
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'default'
==> Generating unified kernel image: '/efi/EFI/Linux/arch-linux.efi'
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'fallback'
==> Generating unified kernel image: '/efi/EFI/Linux/arch-linux-fallback.efi'

systemd-boot automatically finds EFI executables below /efi/EFI/Linux. I do not need to create a traditional loader entry for these UKIs.

Signing systemd-boot and the UKIs

Now I sign the bootloader files and generated UKIs. The -s option is important because it registers each file with sbctl for future automatic signing after package updates.

# sbctl sign -s /efi/EFI/systemd/systemd-bootx64.efi
# sbctl sign -s /efi/EFI/BOOT/BOOTX64.EFI
# sbctl sign -s /efi/EFI/Linux/arch-linux.efi
# sbctl sign -s /efi/EFI/Linux/arch-linux-fallback.efi

The fallback bootloader path is easy to miss. bootctl install places a copy there for firmware which only uses the default removable-media path.

Verify all known files:

# sbctl verify
✓ /efi/EFI/BOOT/BOOTX64.EFI is signed
✓ /efi/EFI/Linux/arch-linux.efi is signed
✓ /efi/EFI/Linux/arch-linux-fallback.efi is signed
✓ /efi/EFI/systemd/systemd-bootx64.efi is signed

After this, reboot into the firmware setup and enable Secure Boot. Do not clear the keys again. The firmware should show Secure Boot enabled and usually also show that custom keys are installed.

Back in Arch, check it:

# sbctl status
Installed:      ✔
Setup Mode:     ✘
Secure Boot:    ✔
Vendor Keys:    microsoft

Kernel updates and recovery

With the UKI configured in the mkinitcpio preset, Arch rebuilds it during kernel updates. Because I registered the UKI with sbctl sign -s, sbctl signs the replacement image through its pacman hook as well.

I still run this after larger updates or after changing boot files:

# sbctl verify

If an update leaves the system unbootable, I would first disable Secure Boot in firmware or enter Setup Mode. Then I boot an Arch ISO, mount the root filesystem and ESP, and chroot into the installation:

# mount /dev/ROOT_PARTITION /mnt
# mount /dev/ESP_PARTITION /mnt/efi
# arch-chroot /mnt
# mkinitcpio -P
# sbctl sign -s /efi/EFI/Linux/arch-linux.efi
# sbctl verify

Do not regenerate keys during recovery unless you also enroll the new public keys into firmware. Usually the original key files in /var/lib/sbctl/keys are all that is needed.

This setup has been nicely boring on my Arch systems, which is exactly what I want from a boot chain. If you know a better way to handle UKI fallback images, let me know.