This is a manual to convert a Debian installation to use EFI Stub for booting instead of relying on a conventional bootloader like GRUB. When used in conjunction with a unified kernel image, it is easy to sign kernel images and encrypt everything except the EFI boot partition while Secure Boot takes care of checking the kernel signature.
Installation is easier on a minimal system as less things can break. Note that this manual is intended for a fresh installation and not for converting your system, although that should be possible the same way. To make things easy, install your system as if you were going to use it normally. This manual will assume the following partitioning scheme:
1) EFI partition
2) /boot partition
3) / partition (possibly encrypted, although that is not mandatory)
Before starting, make sure the following packages are installed (required):
binutils
efibootmgr
efitools
dosfstools
parted
gdisk
You should now have a system that boots using conventional GRUB (and possibly shim's).
This manual will first run through creating a plain unified kernel image containing the initramfs as well as the kernel and the kernel commandline parameters. At the bottom, there is a script that does all these steps automatically, but I would recommend to run through all steps by hand once to see how things are working.
First, a unified kernel image is necessary. To create it, you need the kernel code running on boot.
For Debian/Fedora, this is located at /boot/vmlinuz-$(uname -r)
.
Next, the initial ramdisk is located at /boot/initrd.img-$(uname -r)
.
Additionally, command options for startup are necessary. Your system will have all necessary parameters set by
default during installation, as some things like which microcode to load can depend on your platform and system.
The easiest route is to just adapt them from cat /proc/cmdline
. Note here that the initrd=
parameter can be left
out as that will now be taken care of by systemd-boot.
initrd=\\EFI\\Debian\\initrd.img \ <-- Can be left out
rd.luks.name=58d7fdfa-c5fb-411f-8e2d-dfccd954773f=nvme0n1p3_crypt \
rd.luks.options=allow-discards \
root=UUID=3928b0de-0d86-4afa-b2bd-d1cbc649bfd2 \
rw quiet
Next, the EFI image itself needs to be created by running the following line.
It is possible to substitue .cmdline={..}
for the path to a file that holds your command line options.
$ objcopy \
--add-section .osrel="/etc/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="/proc/cmdline" --change-section-vma .cmdline=0x30000 \
--add-section .linux="/vmlinuz" --change-section-vma .linux=0x40000 \
--add-section .initrd="/initrd.img" --change-section-vma .initrd=0x3000000 \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub /boot/efi/EFI/Debian/unified_debian.efi
Note the last line: /boot/efi/EFI/Debian/unified_debian.efi
may change depending on your distribution. Check the correct
path on your distribution of choice.
Lastly, a new boot entry for the image is necessary in order for the UEFI to know what to boot. The entry can be created by running the following command:
$ efibootmgr \
--create --disk /dev/nvme0n1 --part 1 \
--label "Unified Kernel Image" \
--loader "\EFI\Debian\unified_kernel.efi"
The last line is local to the /boot/efi
directory and here, a backslash is necessary. Adapt the filenames from the previous
command.
Note that the label is not important and you can choose anything you want. Try and not use special characters as that can lead to some nasty problems on some misbehaved UEFI implementations.
Lastly, check if the bootorder is correct. After running efibootmgr -v
, your entry should be at the top of the boot order.
If not, you need to change it so the newly created entry has the highest precedence.
To create a new order, run efibootmgr -o XXXX,YYYY,ZZZZ
where XXXX
is the id of your unified kernel image
and YYYY,ZZZZ,...
and so forth are the remaining entries. On my laptop for example (Lenovo L480), there is a setting in the
BIOS called Unlock boot order
that needs to be disabled for efibootmgr
to be able to overwrite entries.
On some UEFI implementations, the above might not work correctly. In that case, try to set the boot order manually via the UEFI interface of your motherboard. And make sure you are running the most up-to-date version of UEFI firmware. If the above does not work, this is very likely to be a bug in your UEFI and you should probably make your motherboard manufacturer aware.
If you now reboot your system and run efibootmgr
, the output should look something like this:
BootCurrent: 0000
Timeout: 1 seconds
BootOrder: 0000,0001
Boot0000* Unified Kernel (Debian)
Boot0001* Unified Kernel (Recovery)
The BootCurrent
entry reports the image that has been booted and should be the Unified Kernel Image you just created. If
this differs and is your GRUB boot, check if BootOrder
is correct and refer to the above steps.
IMPORTANT: Before doing any of this, disable Secure Boot. There may be some hidden conditions that may cause your firmware to brick.
First, it is helpful to have all the old keys backed up to somewhere safe.
Most motherboards do allow for the original keys to be restored, but better to be safe than sorry.
To read keys, the efi-readvar
utility from the efitools
package will be used.
To read all the keys, run the following commands:
$ efi-readvar -v PK -o old_PK.esl
$ efi-readvar -v KEK -o old_KEK.esl
$ efi-readvar -v db -o old_db.esl
$ efi-readvar -v dbx -o old_dbx.esl
This will store the Platform Key (PK), the Key Exchange Key (KEK) and the database keys in the current directory.
Next, a GUID for the user will be created. This is soley to make identification later on more easy and not necessary.
$ uuidgen --random > GUID.txt
Now, your new personal keys need to be created. To do so, you can either manually run the following pieces of code or alternatively download and run the following script: https://www.rodsbooks.com/efi-bootloaders/mkkeys.sh (Choose something for the common name)
Platform key:
$ openssl req -newkey rsa:4096 -nodes -keyout PK.key \
-new -x509 -sha256 -days 3650 -subj "/CN=my Platform Key/" \
-out PK.crt
$ openssl x509 -outform DER -in PK.crt -out PK.cer
$ cert-to-efi-sig-list -g "$(< GUID.txt)" PK.crt PK.esl
$ sign-efi-sig-list -g "$(< GUID.txt)" -k PK.key \
-c PK.crt PK PK.esl PK.auth
Key Exchange Key:
$ openssl req -newkey rsa:4096 -nodes -keyout KEK.key \
-new -x509 -sha256 -days 3650 -subj "/CN=my Key Exchange Key/" \
-out KEK.crt
$ openssl x509 -outform DER -in KEK.crt -out KEK.cer
$ cert-to-efi-sig-list -g "$(< GUID.txt)" KEK.crt KEK.esl
$ sign-efi-sig-list -g "$(< GUID.txt)" -k PK.key \
-c PK.crt KEK KEK.esl KEK.auth
Signature Database key:
$ openssl req -newkey rsa:4096 -nodes -keyout db.key \
-new -x509 -sha256 -days 3650 -subj "/CN=my Signature Database key/" \
-out db.crt
$ openssl x509 -outform DER -in db.crt -out db.cer
$ cert-to-efi-sig-list -g "$(< GUID.txt)" db.crt db.esl
$ sign-efi-sig-list -g "$(< GUID.txt)" -k KEK.key -c KEK.crt \
db db.esl db.auth
To enroll the newly created keys, the process varies wildly depending on manufacturer and firmware version. If your UEFI can enroll keys from a FAT32 partition like the ESP, it is enough to copy all necessary files to the partition and enroll them manually from within your UEFI:
$ cp *.{cer,esl,auth} /boot/efi/EFI/Debian/keys
It may be advisable to enroll keys in reverse order of their precedence. Keys for secure boot follow a hierarchical order where the PK is more important than the KEK which in turn is more important than the Signature Database. To enroll your keys, start of by clearing everything and then enroll the keys in reverse order: DB -> KEK -> PK. I may have soft-bricked my motherboard the first time I did this in another order and the only remedy was to reflash the mainboard. 1
If that does not work for you, I would recommand to have a look at other possibilities listed in [6].
To sign the kernel with the keys created earlier, run the following command:
$ sbsign --key db.key --cert db.crt --output \
/boot/efi/EFI/debian/unified_kernel.efi \
/boot/efi/EFI/Debian/unified_kernel.efi
After the kernel is signed, everything is ready and you should be able to reboot, enable secure boot and run your system.
Running objcopy
and sbsign
on every kernel update or change is not only tedious but also error-prone.
To automate the process, you can create a script that runs on every update or removal
of the kernel as well as the initramfs.
#!/bin/sh
printf "I: Updating unified kernel image...\n"
# This is a copy of the old image to be able to revert back after a broken update
cp /boot/efi/EFI/Debian/unified_kernel.efi /boot/efi/EFI/Debian/unified_kernel_old.efi
new_kernel_version=$(ls -lrt --time-style=full-iso /lib/modules | cut -d" " -f9 | tail -n1)
printf "I: New kernel versio is $new_kernel_version\n"
# Build a new kernel image
objcopy \
--add-section .osrel="/etc/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="/proc/cmdline" --change-section-vma .cmdline=0x30000 \
--add-section .linux="/boot/vmlinuz-$new_kernel_version" --change-section-vma .linux=0x40000 \
--add-section .initrd="/boot/initrd.img-$new_kernel_version" --change-section-vma .initrd=0x3000000 \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub /boot/efi/EFI/Debian/unified_debian.efi
# Sign the kernel with our keys
sbsign --key db.key --cert db.crt --output \
/boot/efi/EFI/debian/unified_kernel.efi \
/boot/efi/EFI/Debian/unified_kernel.efi
Note that both the .linux
and the .initrd
section may be different between distributions in that debian names the ramdisk
initrd.img-$(uname -r)
while fedora for example names it ìnitramfs-$(uname -r).img
. Adjust accordingly.
This script needs to be located at /etc/kernel/{postinst.d,postrm.d}/zz-update-uki
and
/etc/initramfs/post-update.d/zz-update-uki
. Do this by either copying or symlinking the files there.
It is important that the file starts with zz
as the scripts in each folder are run in alpha-numerical order.
By starting with zz*
, we can assure that it will always run at the end.
It may be advisable to create another slot for the backup copy, should something ever break during an update.
Follow the steps from before to do so (regarding the entry using efibootmgr
).
You are now done and should be able to just reboot and run. The next steps are just cleanup.
During install, the system was setup to use GRUB and all the tools that come with this.
Additionally, the /boot
partition can now be moved into the encrypted volumes as it is no longer necessary to
boot since all necessary components are included inside of the unified kernel image.
The following steps will remove all unnecessary components.
The Debian installer apparently forces you to create a /boot
partition. Since it does not necessarily need
to be a separate partition anymore, lets move it to the root file system.
First, unmount the EFI partition:
umount /boot/efi
Next, copy the /boot
folder. Note the -a
here which preserves all privileges. If this flag
is not present, it may screw up permissions and make Linux unhappy:
cp -a /boot ~/boot_cpy
Then, unmont /boot
:
umount /boot
Now, we can remove the old /boot
directory:
rm -rf /boot
Lastly, recreate /boot
again to create it on the encrypted partition:
mv ~/boot_cpy /boot
To finish this step, update /etc/fstab
and remove separate entry for /boot
as this is now present inside the partition itself.
Since the /boot
partition is now gone, there is some unused space on the drive.
While it would be perfectly fine to leave that empty, it can also be re-purposed as
extra space for the EFI partition. First, let's backup the EFI partition:
cp -a /boot/efi/ ~/efi_back
Next, we unmount the EFI partition:
umount /boot/efi
Now, we repartition the disk:
gdisk /dev/nvme0n1
p (list partitions)
d -> 2 (delete /boot partition)
d -> 1 (delete /boot/efi partition)
n (create new efi partition)
-> [Enter] (partition number, default should be 1)
-> [Enter] (first sector to use)
-> [Enter] (last sector to use)
-> EF00 (EFI partition number)
-> w (write to disk)
To make Linux rediscover partitions, run the follwing:
partprobe /dev/nvme0n1
Next, format the new partition to be used as a EFI partition:
mkfs.fat -F32 /dev/nvme0n1p1
Lastly, we remount the partition and then copy all files from our backup into it:
mount /dev/nvme0n1p1 /boot/efi
cp -a ~/efi_back/EFI /boot/efi/
To finish things up, grab the new EFI partition UUID by typing blkid | grep EFI
and then update /etc/fstab
with
the new UUID, rebuild the kernel image by rerunning the shell scripte created earlier and recreate
the efibootmgr
entry. Afterwards, you can safely reboot.
As GRUB is still present on your system, it can now be removed as a last step. To do so, lets first remove all GRUB packages:
apt purge grub-efi-amd64 grub-efi-amd64-bin grub-efi-amd64-signed grub2-common
The rest of the dependencies can then be removed by running autoremove:
apt autoremove
Lastly, all remnants from the EFI partition can be removed:
rm fbx64.efi grub.cfg grubx64.efi BOOTX64.CSV mmx64.efi
As GRUB is still present on your system, it can now be removed as a last step. To do so, lets first remove all GRUB packages.
As these are protected, we first need to remove all entries relating to grub and shims in /etc/dnf/protected.d/
.
These have names like /etc/protected.d/grub2-pc.x86_64.conf
. Once they are removed, we can
uninstall all modules by running
dnf remove grub2*
Lastly, all remnants from the EFI partition can be removed:
rm grub.cfg
[1] https://wiki.debian.org/EFIStub
[2] https://visualplanet.org/blog/?p=460
[3] https://superuser.com/questions/1230741/how-to-resize-the-efi-system-partition
[4] https://www.cogitri.dev/posts/04-secure-boot-with-unified-kernel-image/
[5] https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot
Footnotes
-
This may have also been caused by a faulty UEFI implementation. I did not care enough to look into this more deeply and since the hardware was fairly new at the time, support forums did not have any information that described similar things yet. ↩