Enhanced UEFI Secure Boot of Linux+cmdline & initramfs with dracut on Debian Buster

  • Last Update:2022-03-23
  • Version:001
  • Language:

Abstract

During the project explained here we needed an easy way of obtaining UEFI Secure Boot for both the Linux kernel and it's command line parameters as well as the initramfs (which we will also modify for that project but not here), that is the most compatible (non-intrusive) with existing GNU/Linux distributions.

Note: in this document, all shell commands prefixed by $ can and should be run as a regular user, and those prefixed by # must be run as root.

Why dracut

Dracut is probably the most modern tool to generate initramfs images, it can also generate UEFI applications that embed the Linux kernel, it's command line parameters, and the initramfs. It is especially interesting in this scenario because this means we can sign this UEFI application for Secure Boot, set it as the boot program and be done with it. It's easy, simple and efficient but also more secure due to it including kernel command line parameters and initramfs without any extra keys or signature. It bypasses the GRUB bootloader completely.

--uefi
Instead of creating an initramfs image, dracut will create an UEFI executable, which can be executed by an UEFI BIOS. The default output filename is <EFI>/EFI/Linux/linux-$kernel$-<MACHINE_ID>-<BUILD_ID>.efi. <EFI> might be /efi, /boot or /boot/efi depending on where the ESP partition is mounted. The <BUILD_ID> is taken from BUILD_ID in /usr/lib/os-release or if it exists /etc/os-release and is left out, if BUILD_ID is non-existant or empty.
--no-machineid
affects the default output filename of --uefi and will discard the <MACHINE_ID> part.
--uefi-stub <FILE>
Specifies the UEFI stub loader, which will load the attached kernel, initramfs and kernel command line and boots the kernel. The default is $prefix/lib/systemd/boot/efi/linux<EFI-MACHINE-TYPE-NAME>.efi.stub or $prefix/lib/gummiboot/linux<EFI-MACHINE-TYPE-NAME>.efi.stub
--uefi-splash-image <FILE>
Specifies the UEFI stub loader’s splash image. Requires bitmap (.bmp) image format.
--kernel-image <FILE>
Specifies the kernel image, which to include in the UEFI executable. The default is /lib/modules/<KERNEL-VERSION>/vmlinuz or /boot/vmlinuz-<KERNEL-VERSION>

dracut's manual

Setting up Debian Buster in a VM with UEFI and SecureBoot enabled

The author is using Fedora 32 as their main system, it has the advantage of having OVMF pre-installed with libvirt and virt-manager (GUI tool for libvirt/QEMU/KVM), if you are having trouble selecting the OVMF firmware, you should seek information on how to install and configure it on your system.

  • In virt-manager, go to File->New Virtual Machine and follow through the instructions until last step
  • On last step, tick "Customize configuration before install" then Finish
  • Go to Overview then at the bottom Firmware, select UEFI OVMF with "secboot" in the filename, then Apply and Begin Installation.
  • Perform a standard installation
  • You can check Secure Boot status with:
$ mokutil --sb-state
SecureBoot enabled

Installing required packages

  • We will need dracut 0.50 or later (required for signing generated UEFI applications), it's not available for buster but for bullseye. Bullseye deb packages work fine when installed onto Buster. You can get them from dracut-core (amd64) and dracut (all) - once downloaded from your preferred mirror, you can install them as root with:
# apt install ./path/to/dracut.deb ./path/to/dracut-core.deb

Make sure you do not have dracut or dracut-core installed before that, and it's important to use the dot and slash at the beginning of paths, otherwise apt will interpret these as regular package names.

  • You will also need the openssl, binutils, efitools and sbsigntool packages.
  • For inclusion in the initramfs so that it is more generic; multipath-tools, lvm2, nvme-cli, dmraid, mdadm, btrfs-progs and rng-tools packages. FIXME: Gain better confidence on initramfs being generic, will be more robust when tested appropriately in several (Nexedi's production) systems
  • Depending on your main file system, you will need tools to set file system labels. For ext* file systems, this is the e2fsprogs package.

Generating signing keys

For our purpose, we only need a Signature Database (DB) signing key;

We can create one with:

$ openssl req -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj "/CN=Nexedi's UEFI Signature Database key/" -out db.crt

Please adapt parameters as needed, such as the Common Name (CN), or the algorithm and key size (as UEFI Secure Boot supports and allows).

UEFI generally only accepts public keys in DER format, so we will convert our key to such format:

$ openssl x509 -outform DER -in db.crt -out db.cer

And that's it. Keep your private key in a safe place, also it's recommended to add a passphrase on it, check openssl's manual for this.

Generating our signed UEFI boot application with dracut

Debian ships default config for dracut in /etc/dracut.conf and /etc/dracut.conf.d/* - this might set unwanted parameters, so make sure to comment those or specify explicit custom configuration and configuration directories on the command line. You can use an empty configuration directory to discard.

First, the dracut configuration file, we will save it as dracut.conf:

hostonly=no
hostonly_cmdline=no
kernel_cmdline="root=LABEL=ROOT"
reproducible=yes
compress=xz
uefi=yes
uefi_stub=/usr/lib/systemd/boot/efi/linuxx64.efi.stub
uefi_secureboot_cert=/root/db.crt
uefi_secureboot_key=/root/db.key

Adapt paths to your needs, such as /root/db.crt and /root/db.key - we also need to set a common label for our main disk, all machines that use this UEFI boot application will need to have this same label set.

After you figured the device path for your main file system, if it is an ext* file system, you can set it using:

# e2label /dev/vda2 ROOT

Adapt your device path, of course.

Then it is time we generate our signed UEFI boot application for real:

# mkdir -p /tmp/dracut-empty
# dracut -c /path/to/dracut.conf --confdir /tmp/dracut-empty
# rm -rf /tmp/dracut-empty

It is now generated! Take note of the path after "Creating signed UEFI image file" in the standard output, we'll need it later.

Adding our Signature Database (DB) public key to the UEFI BIOS

Since we use OVMF here, that's what we will cover, but you should find similar instructions for your UEFI BIOS online.

Before rebooting, we need to make our public key available on a file system that the UEFI BIOS can read, we can use the EFI partition for that. Under Debian Buster, it is /boot/efi by default. Make sure to copy the DER version of our public key, since most UEFI BIOS can only read that format;

# cp /path/to/db.cer /boot/efi

Then, reboot and during boot, hit the F2 key repeatedly until reaching the UEFI BIOS configuration screen:

Then press ENTER on Device Manager,

Press ENTER on Secure Boot Configuration,

Change the "Secure Boot Mode" to "Custom Mode", then select the "Custom Secure Boot Option" entry that just appeared and ENTER,

Select "DB Options" then ENTER, that's where we will enroll our Signature Database (DB) public key, select "Enroll Signature" then press ENTER, "Enroll Signature Using File" and ENTER again. Select your EFI partition then ENTER, then select your DER certificate and ENTER.

Next, select "Commit Changes and Exit" and ENTER. You are done now! Press ESC to go back, then F10 then Y to save just to be sure. Then you can press ESC until you reach the main menu again then select "Continue" and ENTER. You should boot as normal, we have just one last step until we can boot onto our UEFI boot application.

Adding an EFI boot entry for our signed UEFI application

When back in your booted Debian system, remember the path to our signed UEFI application generated by dracut? We'll need it to add the boot entry.

First, you must strip the mount point part of the path, so that if the path starts with /boot/efi/EFI/Linux/linux-4.19.0-10-amd64-cfeeebb4b2274ed7aad5cddf9153dc08.efi you must strip /boot/efi to only have /EFI/Linux/linux-4.19.0-10-amd64-cfeeebb4b2274ed7aad5cddf9153dc08.efi left. This is because the UEFI BIOS does not know about our Linux /boot/efi mount point, it only sees a partition and browses it from it's root.

Finally, after adapting to your situation (see below), run:

efibootmgr --quiet --create --disk /dev/vda1 --label 'debian UEFI' --loader /EFI/Linux/linux-4.19.0-10-amd64-cfeeebb4b2274ed7aad5cddf9153dc08.efi

Replace /dev/vda1 with your system's EFI partition, you can find out about it using fdisk:

# fdisk -l
Disk /dev/vda: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 1D837728-20BF-4705-9FB1-5E3D6659481E

Device        Start      End  Sectors  Size Type
/dev/vda1      2048  1050623  1048576  512M EFI System
/dev/vda2   1050624 39880703 38830080 18.5G Linux filesystem
/dev/vda3  39880704 41940991  2060288 1006M Linux swap
 

You can customize the "debian UEFI" boot entry label as well, and of course, use your own path for the loader, that's our signed UEFI boot application.

Finally, reboot and we are done! The UEFI boot application should now take priority over GRUB on boot. You can notice that all of this worked if you do not see GRUB during boot! You can also find out by running: cat /proc/cmdline and see if that's the command line parameters set in the dracut.conf file earlier.

Automation

This process should definitely be automated. For our purposes described in Abstract, we want to ensure that our initramfs is the most generic possible (at least for one single GNU/Linux distribution) and maintain packages in tandem with the distribution (e.g. kernel versions). We also want to sign the UEFI boot application and maintain private keys on a known-safe system separately. We want to automate registering the Signature Database (DB) public key into UEFI BIOS, probably through IPMI. We would also want to lock the UEFI BIOS behind a password so that intruders cannot override the SecureBoot configuration, same thing for IPMI.

References

Dracut UEFI Unified Kernel Image with Secure Boot Signing

Unified Extensible Firmware Interface/Secure Boot - ArchWiki

$ man dracut

$ man dracut.conf

How to Change Linux Partition Label Names on EXT4 / EXT3 / EXT2 and Swap