Skip to main content

Secure Boot

Secure Boot is a security feature found in the UEFI standard, designed to add a layer of protection to the pre-boot process: by maintaining a cryptographically signed list of binaries authorized or forbidden to run at boot, it helps in improving the confidence that the machine core boot components (boot manager, kernel, initramfs) have not been tampered with.

Preparations

To determine the current state of Secure Boot execute:

bootctl status

The output looks something like this:

System:
     Firmware: UEFI 2.70 (American Megatrends 5.15)
  Secure Boot: enabled
   Setup Mode: user
 Boot into FW: supported
...

In order to proceed "Secure Boot" should say "enabled" and "Setup Mode" should say "setup".

If the command displays the "Setup Mode" as "user" you need to enter your firmware settings and put the firmware into "setup" mode to proceed. This can usually be achieved by wiping the key store of the firmware.

Installation

For the most straigh-forward Secure Boot toolchain install:

pacman -S sbctl

Kernel Command Line Parameters

For the sake of clarity, all parameters are stored in environment variables first and then written to a file with the echo command.

Saving parameters to a file has several advantages:

  • We can always refer to this file to verify which parameters are written to the unified kernel image
  • Subsequent changes can be made more easily by just editing this file
  • The file can easily be included in the configuration of sbupdate and both configurations are cleanly separated from each other
Variable Description
CRYPT_UUID The UUID of the LUKS container, as stated by blkid
CRYPT_NAME The device mapper name which should be used for the unlocked LUKS container (optional, recommended)
ROOT Root file system device aspecified by persistent block device naming
RESUME SWAP space for restoring memory from hibernate (optional)
CMDL Misc. kernel command line parameters
ROOTFLAGS Flags to be passed to the root file system

Command substitution ($(command)) makes it easy, reliable and scriptable to find certain values for these variables.

For example, to get the UUID of the LUKS container simply do (assuming /dev/sda1 is the LUKS container):

export CRYPT_UUID=$(blkid -s UUID -o value /dev/sda1)

The command blkid -s UUID -o value /dev/sda1 just returns the UUID of the given block device. Appending export saves it in an environment variable called CRYPT_UUID which can be read by issuing echo $CRYPT_UUID.

Examples

The following examples show different ways of preparing the final kernel command line to successfully boot a machine with Secure Boot. The output will be written to /etc/kernel/cmdline as this is the place sbctl expects the kernel command line to reside.

busybox-based initramfs

For busybox-based initramfs the LUKS container must be passed with the cryptdevice parameter. The cryptdevice parameter is passed the path to an encrypted block device, followed by a : with the desired device mapper name. Usage of persistent block device naming (i.e. the UUID of the device as returned by blkid) is strongly recommended.

NOTE: This is not relevant for LVM logical volumes as the /dev/VolumeGroupName/LogicalVolumeName device paths already are persistent. If the LUKS container resides inside an LVM logical volume you could simply supply:

cryptdevice=/dev/VolumeGroupName/LogicalVolumeName:DeviceMapperName

NOTE: By default, dm-crypt does not allow TRIM for SSDs for security reasons (information leak). To override this behavior, append :allow-discards as an option parameter after the device mapper name.

For an LVM on LUKS setup (with SWAP for resume) the variables could look like this:

export CRYPT_UUID=$(blkid -s UUID -o value /dev/sda1)
export CRYPT_NAME=cryptroot
export ROOT=/dev/mapper/vg0-lv_root
export RESUME=/dev/mapper/vg0-lv_swap
export CMDL=rw quiet splash add_efi_memmap
export ROOTFLAGS=subvol=@

echo cryptdevice=UUID=$CRYPT_UUID:$CRYPT_NAME root=$ROOT resume=$RESUME $CMDL rootflags=$ROOTFLAGS > /etc/kernel/cmdline

systemd-based initramfs

For systemd-based initramfs the LUKS container must be passed with the rd.luks.* parameter. The LUKS container can either be passed with the rd.luks.uuid or rd.luks.name parameter. Both take the UUID of the LUKS container, with rd.luks.name additionaly taking a parameter for a device mapper name. Usage of persistent block device naming (i.e. the UUID of the device as returned by blkid) is mandatory.

When using rd.luks.name as the boot parameter, rd.luks.uuid can be omitted as it is implied.

NOTE: By default, dm-crypt does not allow TRIM for SSDs for security reasons (information leak). To override this behavior, add rd.luks.options=discard as an additional parameter.

For a LUKS on LVM setup the variables could look like this:

export CRYPT_UUID=$(blkid -s UUID -o value /dev/sda1)
export CRYPT_NAME=cryptroot
export ROOT=/dev/mapper/$CRYPT_NAME
export CMDL=rw quiet splash add_efi_memmap
export ROOTFLAGS=subvol=@

echo rd.luks.name=$CRYPT_UUID=$CRYPT_NAME root=$ROOT $CMDL rootflags=$ROOTFLAGS > /etc/kernel/cmdline

Generating keys

FURTHER READING: The Meaning of all the UEFI Keys

Secure Boot implementations use these keys:

Key Type Description
Platform Key (PK) Top-level key
Key Exchange Key (KEK) Keys used to sign Signatures Database and Forbidden Signatures Database updates
Signature Database (db) Contains keys and/or hashes of allowed EFI binaries
Forbidden Signatures Database (dbx) Contains keys and/or hashes of denylisted EFI binaries

To generate new keys and store them under /usr/share/secureboot/keys/:

sbctl create-keys

Enroll keys in firmware

WARNING: Replacing the platform keys with your own can end up bricking your machine, making it impossible to get into the UEFI/BIOS settings to rectify the situation. This is due to the fact that some device firmware (OpROMs, e.g. GPU firmware), that gets executed during boot, may be signed using Microsoft's keys. If in doubt, run sbctl enroll-keys --microsoft (enrolling Microsoft's Secure Boot keys alongside your own custom ones) or sbctl enroll-keys --tpm-eventlog (if your machine has a TPM and you don't need or want Microsoft's keys) to prevent bricking your machine.

ATTENTION: Make sure your firmware's Secure Boot mode is set to setup mode! You can do this by going into your firmware settings and wiping the factory default keys. Additionally, keep an eye out for any setting that auto-restores the default keys on system start.

TIP: If you plan to dual-boot Windows, run sbctl enroll-keys --microsoft to enroll Microsoft's Secure Boot keys along with your own custom keys.

To enroll your keys, simply:

sbctl enroll-keys

Automate unifying and resigning kernel images on update

Starting with v31 mkinitcpio is able to create unified kernel images out-of-the-box. The author of sbctl recommends using the initrd generator instead of sbctl bundle.

HINT: If not instructed otherwise, mkinitcpio will automatically source the /etc/kernel/cmdline we created earlier to embed kernel commandline parameters into the image. Using a different file requires the --cmdline option.

To enable unified kernel image generation in mkinitcpio, edit the *.preset responsible for initrd generation, e.g. /etc/mkinitcpio.d/linux.preset, and add the following:

  • If your system requires microcode, add ALL_microcode=(/boot/*-ucode.img)
  • Add a PRESET_uki= parameter for each item in the PRESETS= array
  • Optionally, append any parameters that mkinitcpio supports to each PRESET_options= line to customize the generated image to your needs (for example --cmdline /etc/kernel/fallback_cmdline in fallback_options to use a different file for the cmdline of the fallback image, e.g. without quiet)

TIP: You could even go as far as to supply an entirely different kernel and initrd, e.g. linux-lts:

fallback_kver="/boot/vmlinuz-linux-lts"
fallback_image="/boot/initramfs-linux-lts-fallback.img"
fallback_uki="/efi/EFI/Linux/archlinux-linux-lts-fallback.efi"
fallback_options="--cmdline /etc/kernel/fallback_cmdline"

As an example, the finished edit should look something like this (added lines marked with # NEW for ease of identification, ESP is assumed at /efi):

# mkinitcpio preset file for the 'linux' package

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"
ALL_microcode=(/boot/*-ucode.img)                            # NEW

PRESETS=('default' 'fallback')

#default_config="/etc/mkinitcpio.conf"
default_image="/boot/initramfs-linux.img"
default_uki="/efi/EFI/Linux/archlinux-linux.efi"             # NEW
#default_options=""

#fallback_config="/etc/mkinitcpio.conf"
fallback_image="/boot/initramfs-linux-fallback.img"
fallback_uki="/efi/EFI/Linux/archlinux-linux-fallback.efi"   # NEW
fallback_options="-S autodetect"

Next, add the images to the list of files to be signed (one at a time):

sbctl sign --save /efi/EFI/Linux/archlinux-linux.efi
sbctl sign --save /efi/EFI/Linux/archlinux-linux-fallback.efi

If you plan on using a boot loader, you will also need to add its *.efi executable(s) to the list of files to be signed, e.g. systemd-boot:

NOTE: This is the manual method. If you'd rather automate the bootloader update process, skip to the next section.

sbctl sign --save /efi/EFI/BOOT/BOOTX64.EFI
sbctl sign --save /efi/EFI/systemd/systemd-bootx64.efi

Upon system upgrades, pacman will call sbctl to re-sign the files listed in sbctl's database.

Automate systemd-boot updates and signing

systemd comes with a systemd-boot-update.service unit file to automate updating the bootloader whenever systemd is updated. However, it only updates the bootloader after a reboot, by which time sbctl has already run the signing process. This would necessitate manual intervention.

Recent versions of bootctl look for a .efi.signed file before a regular .efi file when copying bootloader files during install and update operations. So to integrate better with the auto-update functionality of systemd-boot-update.service, the bootloader needs to be signed ahead of time.

sbctl sign --save -o /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed /usr/lib/systemd/boot/efi/systemd-bootx64.efi

This will add the source and target file paths to sbctl's database. The pacman hook included with sbctl will trigger whenever a file in usr/lib/**/efi/*.efi* changes, which will be the case when systemd is updated and a new version of the unsigned bootloader is written to disk at /usr/lib/systemd/boot/efi/systemd-bootx64.efi.

Finally, enable the systemd-boot-update.service unit:

systemctl enable systemd-boot-update

Now when systemd is updated the signed version of the systemd-bootx64.efi booloader will be copied to the ESP after a reboot, completely automating the bootloader update and signing process!