Back to Blog

Implementing Secure Boot on Arch Linux

A comprehensive guide to manually setting up Secure Boot with custom keys

Since it's not very well documented on the internet, I'll share my experience on implementing Secure Boot on my machine.

If you want a deeper understanding of how it works I suggest Rodsbooks's Secure Boot, from which I learned it myself. Obviously the ArchWiki is another great source.

Introduction

Secure Boot is a security feature provided by UEFI that verifies the first boot components (kernel, initramfs, bootloader) using a public key infrastructure. The system maintains a database of trusted keys (db) and forbidden keys (dbx). These keys are signed by KEKs (Key Exchange Keys) that computers ship with — typically one from Microsoft and one from the motherboard manufacturer. At the top sits a single Platform Key (PK), provided by the motherboard manufacturer, which signs the KEKs.

MOKs (Machine Owner Keys) are an alternative to db keys used by signed bootloaders like shim or PreLoader, but they are not a standard part of Secure Boot. I won't use them here since I'm focusing on the standard, minimalist approach — though signed bootloaders are the easiest way to get started. If your goal is not full control of your machine or you don't plan to remove the Microsoft keys from the firmware, I'd recommend going that route instead. If you do use your own keys, consider removing the Microsoft keys — but be aware this can brick some hardware, especially laptops, where device firmware is signed by Microsoft.

Generating the keys

The first step is to create your own keys. Install sbsigntools, openssl, and efitools. To speed up the process you can use Rodsbooks's script:

#!/bin/bash
# Copyright (c) 2015 by Roderick W. Smith
# Licensed under the terms of the GPL v3

echo -n "Enter a Common Name to embed in the keys: "
read NAME

openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME PK/" -keyout PK.key \
        -out PK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME KEK/" -keyout KEK.key \
        -out KEK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME DB/" -keyout DB.key \
        -out DB.crt -days 3650 -nodes -sha256
openssl x509 -in PK.crt -out PK.cer -outform DER
openssl x509 -in KEK.crt -out KEK.cer -outform DER
openssl x509 -in DB.crt -out DB.cer -outform DER

GUID=`python3 -c 'import uuid; print(str(uuid.uuid1()))'`
echo $GUID > myGUID.txt

cert-to-efi-sig-list -g $GUID PK.crt PK.esl
cert-to-efi-sig-list -g $GUID KEK.crt KEK.esl
cert-to-efi-sig-list -g $GUID DB.crt DB.esl
rm -f noPK.esl
touch noPK.esl

sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
                  -k PK.key -c PK.crt PK PK.esl PK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
                  -k PK.key -c PK.crt PK noPK.esl noPK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
                  -k PK.key -c PK.crt KEK KEK.esl KEK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
		  -k KEK.key -c KEK.crt db DB.esl DB.auth

chmod 0600 \*.key

echo ""
echo ""
echo "For use with KeyTool, copy the *.auth and *.esl files to a FAT USB"
echo "flash drive or to your EFI System Partition (ESP)."
echo "For use with most UEFIs' built-in key managers, copy the *.cer files;"
echo "but some UEFIs require the *.auth files."
echo ""

Now you should have all the keys you need. Move them to a folder available at boot and restrict permissions so they are not world-readable. Keep in mind that even with filesystem protection, the keys sit on disk in plain text — anyone with root access can read them and sign arbitrary binaries. For this reason, if you go the self-signing route, store the keys on an encrypted partition and keep the EFI binaries on a separate partition. Whatever you choose, set a firmware password; without one, anyone with physical access can simply add their own keys or disable Secure Boot.

Signing EFI binaries

Let's sign all the EFI binaries checked during boot. For example, sign the bootloader at /boot/EFI/<GRUBDIR>/grubx64.efi:

sbsign --key /root/keys/DB.key --cert /root/keys/DB.crt --output grubx64.efi grubx64.efi

And the kernel at /boot/vmlinuz-linux:

sbsign --key /root/keys/DB.key --cert /root/keys/DB.crt --output vmlinuz-linux vmlinuz-linux

If these commands produce errors like warning: data remaining[1231832 vs 1357089]: gaps between PE/COFF sections?, these can safely be ignored. These commands will overwrite your unsigned files, so change the output filename if you want to keep the originals.

To verify that the EFI binaries have been signed successfully:

sbverify --list /boot/vmlinuz-linux

If the output shows the issuer name you chose during key generation, the executable has been correctly signed.

The problem with signing individual files is that it won't protect the initramfs from tampering, so we need to build a unified kernel image. Start by configuring initramfs to be an uncompressed cpio archive: edit /etc/mkinitcpio.conf and add COMPRESSION="cat" at the end. It's also worth disabling the fallback preset in /etc/mkinitcpio.d/linux.preset by removing 'fallback' from PRESETS('default' 'fallback'), since protecting it would require building a second unified kernel. Then prepare the input files:

First, save the current kernel command line to a file:

cat /proc/cmdline > cmdline.txt

Then, if you use microcode (e.g. intel-ucode), merge it with the initramfs:

cat /boot/intel-ucode.img /boot/initramfs-linux.img > /tmp/boot/initramfs.img

Move the kernel and the merged initramfs into a temporary working directory (/tmp/boot), then run the following command to produce the unified image:

/usr/bin/objcopy \
    --add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000 \
    --add-section .cmdline=./cmdline.txt --change-section-vma .cmdline=0x30000 \
    --add-section .linux=/tmp/boot/vmlinuz-linux --change-section-vma .linux=0x40000 \
    --add-section .initrd=/tmp/boot/initramfs.img --change-section-vma .initrd=0x3000000 \
    /usr/lib/systemd/boot/efi/linuxx64.efi.stub /tmp/boot/unified-kernel.efi

Now sign the unified kernel image with the sbsign command shown above and configure your bootloader to load it. For GRUB2, append these lines to /etc/grub.d/40_custom, inserting your own UUIDs:

menuentry "Arch Linux" --class arch --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-<root partition uuid>' {
	insmod fat
	insmod chain
	search --no-floppy --set=root --fs-uuid <partition's uuid where unified-kernel.img is saved>
	chainloader /unified-kernel.img
}

You can also use grub-customizer for this — the important thing is that the boot entry contains chainloader /unified-kernel.img.

Automating the signing process

To make everything easier, we can use a pacman hook to automatically re-sign EFI binaries on every kernel or initramfs update. The script I use is the following — just modify the variables to suit your setup:

#!/bin/bash

FILE=$(echo $1 | sed 's/boot\///')
BOOTDIR=/boot
CERTDIR=/root/keys # the directory where the keys are stored
KERNEL=$1
INITRAMFS="/boot/intel-ucode.img /boot/initramfs-$(echo $FILE | sed 's/vmlinuz-//').img"
EFISTUB=/usr/lib/systemd/boot/efi/linuxx64.efi.stub
BUILDDIR=/tmp/_boot
OUTIMG=/boot/$(echo $FILE | sed 's/vmlinuz-//').img
CMDLINE=/etc/cmdline # this file is a copy of /proc/cmdline

mkdir -p $BUILDDIR

cat ${INITRAMFS} > ${BUILDDIR}/initramfs.img

/usr/bin/objcopy \
    --add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000 \
    --add-section .cmdline=${CMDLINE} --change-section-vma .cmdline=0x30000 \
    --add-section .linux=${KERNEL} --change-section-vma .linux=0x40000 \
    --add-section .initrd=${BUILDDIR}/initramfs.img --change-section-vma .initrd=0x3000000 \
    ${EFISTUB} ${BUILDDIR}/combined-boot.efi

/usr/bin/sbsign --key ${CERTDIR}/DB.key --cert ${CERTDIR}/DB.crt --output ${BUILDDIR}/combined-boot-signed.efi ${BUILDDIR}/combined-boot.efi

cp ${BUILDDIR}/combined-boot-signed.efi ${OUTIMG}

rm -r $BUILDDIR

Finally, create /etc/pacman.d/hooks/secure-boot.hook with the following content:

[Trigger]
Operation = Install
Operation = Upgrade
Type = Path
Target = usr/lib/modules/*/vmlinuz

[Trigger]
Operation = Install
Operation = Upgrade
Operation = Remove
Type = Path
Target = usr/lib/initcpio/*

# remove or change the following if you don't use intel-ucode
[Trigger]
Operation = Install
Operation = Upgrade
Operation = Remove
Type = Package
Target = intel-ucode

[Trigger]
Operation = Upgrade
Type = Package
Target = systemd

[Action]
Description = Updating UEFI kernel images...
When = PostTransaction
Exec = /bin/sh -c '/root/secure-boot/autosign.sh "boot/vmlinuz-linux"'
NeedsTargets

This hook fires whenever systemd or intel-ucode is upgraded, or when initramfs files or kernel images change. To test it, run sudo pacman -S linux — if the post-transaction output includes "Updating UEFI kernel images...", the hook is working correctly.

Installing the keys in the firmware

If everything worked, you can now install the keys in your firmware. This step differs for every UEFI, so I won't go into detail, but the general process is the same. First, put your motherboard into Secure Boot setup mode — on a ThinkPad X240 this means booting into setup (F1 at the splash screen), going to Security → Secure Boot → Reset to Setup Mode, then exiting with saved changes.

Next, copy /usr/share/efitools/efi/KeyTool.efi to your ESP (e.g. /boot) and boot from it. On the main menu, save the existing keys before touching anything. Then select "Edit Keys" and remove any keys you no longer want — you can remove Microsoft ones, but only if you don't dual-boot and your hardware doesn't need them for firmware components to work. Add your own keys in the correct order: db first (select the db entry → "Add New Key" → DB.esl), then KEK (KEK.esl), and finally PK (select "The Platform Key (PK)" → "Replace Key(s)" → PK.auth). Exit KeyTool and restart. To verify, try booting from an unsigned USB drive — the process should be blocked.

Conclusion

The manual implementation of Secure Boot is annoying and very error-prone — it took me two days to get it working correctly. But at least I came away understanding how a system can be secured from the very first instruction at boot. My final advice: encrypt your root partition with LUKS as well, and enjoy your secured boot process! 🔒