Enabling Secure Boot With REFInd and a Custom Key
Even though it brings more trouble than anything else, we are going to see how to enable Secure Boot on a Linux distribution such as Pop!_OS with the rEFInd boot manager. The principle should be the same with GRUB, namely signing the entire boot chain.
The context
The reference machine for this article:
- UEFI firmware with Secure Boot available and the original Microsoft keys
- dual boot Windows 11 and Pop!_OS 24.04 LTS (COSMIC)
- rEFInd as the boot manager
The chain of trust
Secure Boot is a chain: each link verifies the next one before handing over control. If a single link is not signed by a trusted key, everything stops right there.
The firmware only trusts the certificates present in its db database. In practice, on an off-the-shelf machine, those are Microsoft’s and the manufacturer’s. Signing code yourself therefore amounts to inserting your own key somewhere in that chain. The simplest approach is to keep using the Windows key to boot Windows and to add your own key for the Linux side.
There is a boot loader called shim, signed with Microsoft’s key, that we can configure to accept binaries signed with our own key.
flowchart TD
FW["UEFI firmware
db database: Microsoft + ASUS"]
SHIM["shimx64.efi
signed by Microsoft"]
MOK[("MOK
our custom key")]
REFIND["grubx64.efi = rEFInd
signed with the MOK"]
DRIVER["drivers_x64/ext4_x64.efi
signed with the MOK"]
KERNEL["/boot/vmlinuz
signed with the MOK"]
WIN["Windows Boot Manager
signed by Microsoft"]
FW -->|verifies via db| SHIM
FW -->|verifies via db| WIN
SHIM -->|loads the fixed name grubx64.efi| REFIND
MOK -.->|secondary key database| SHIM
REFIND -->|reads the ext4 partition| DRIVER
DRIVER --> KERNEL
flowchart TD
FW["UEFI firmware
db database: Microsoft + ASUS"]
SHIM["shimx64.efi
signed by Microsoft"]
MOK[("MOK
our custom key")]
REFIND["grubx64.efi = rEFInd
signed with the MOK"]
DRIVER["drivers_x64/ext4_x64.efi
signed with the MOK"]
KERNEL["/boot/vmlinuz
signed with the MOK"]
WIN["Windows Boot Manager
signed by Microsoft"]
FW -->|verifies via db| SHIM
FW -->|verifies via db| WIN
SHIM -->|loads the fixed name grubx64.efi| REFIND
MOK -.->|secondary key database| SHIM
REFIND -->|reads the ext4 partition| DRIVER
DRIVER --> KERNEL
flowchart TD
FW["UEFI firmware
db database: Microsoft + ASUS"]
SHIM["shimx64.efi
signed by Microsoft"]
MOK[("MOK
our custom key")]
REFIND["grubx64.efi = rEFInd
signed with the MOK"]
DRIVER["drivers_x64/ext4_x64.efi
signed with the MOK"]
KERNEL["/boot/vmlinuz
signed with the MOK"]
WIN["Windows Boot Manager
signed by Microsoft"]
FW -->|verifies via db| SHIM
FW -->|verifies via db| WIN
SHIM -->|loads the fixed name grubx64.efi| REFIND
MOK -.->|secondary key database| SHIM
REFIND -->|reads the ext4 partition| DRIVER
DRIVER --> KERNEL
|
|
Three points are worth highlighting on this diagram.
shim loads a file with a hardcoded name. It looks for grubx64.efi in its own directory, whatever loader is actually being used. So we copy refind_x64.efi under that name.
The Linux kernel is not on the ESP. Unlike Windows, whose entire boot payload fits on the FAT32 partition, the kernel lives in /boot, on the ext4 root. But UEFI firmware can only read FAT. rEFInd works around this with its own filesystem drivers, in drivers_x64/.
Those drivers are EFI code like any other. Under Secure Boot, they therefore have to be signed. This is the link everyone forgets, because it is invisible as long as Secure Boot is disabled: without a signature, rEFInd boots perfectly, displays its menu, but can no longer read the Linux partition. The Pop!_OS entry disappears from the menu, or fails if it was declared manually in refind.conf.
A note about Pop!_OS
Pop!_OS does not use GRUB but systemd-boot, driven by kernelstub, which copies the kernel and the initrd into the ESP under \EFI\Pop_OS-<root-uuid>\. If you boot through systemd-boot, it is that copy you need to sign, not /boot/vmlinuz-* — signing the latter would have no effect.
With rEFInd the behaviour is different: it reads /boot directly on the ext4 root. To find out which case you are in, look at the kernel command line after a successful boot:
|
|
An initrd=\boot\initrd.img with backslashes and no BOOT_IMAGE= indicates that rEFInd loaded the kernel from the Linux partition. A path under \EFI\Pop_OS-… indicates that it went through systemd-boot.
In my case, I chose not to go through systemd-boot in order to avoid an extra step.
Prerequisites
|
|
sbsigntool provides sbsign and sbverify, mokutil lets you manage the MOK database, and shim-signed provides the shim binary signed by Microsoft along with MokManager.
1. Generate the key
The keys will be used by a hook running as root during kernel updates. So we store them in /root.
I use the codeSigning option to sign EFI binaries. This makes it possible to reuse that same key to sign kernel modules. Without it, the NVIDIA drivers for instance will not work.
-nodes generates a private key without a passphrase: this is essential for automatic signing to work without any intervention. In exchange, anyone who gains root access to the machine can sign code that will boot with Secure Boot enabled. Protect the directory accordingly:
mokutil expects a certificate in DER format, so we produce a conversion:
We keep both formats: the .crt (PEM) for sbsign and sbverify, the .der for mokutil.
2. Enroll the key in the MOK
|
|
The command asks for a password. It is single-use: it will only be needed at the next boot to confirm the enrollment. Pick something easy to type on a QWERTY keyboard, because MokManager ignores the keyboard layout configured in the system.
mokutil --import only records a request. You then have to reboot. A blue screen appears, before any boot manager:
- Enroll MOK
- Continue
- Yes
- enter the password chosen in the previous step
- Reboot
If you let the countdown expire or if you choose Continue boot, the request is dropped and the key is not enrolled. You then have to start over from mokutil --import.
To check, once rebooted:
|
|
One more pitfall: when Secure Boot is disabled, shim does not copy the full MokList into the runtime variable that mokutil reads. So it is perfectly normal to only see the Canonical certificate in that situation, without it meaning that your key has vanished. Run the check again with Secure Boot enabled if you have any doubt. To remove the ambiguity entirely, the best proof remains to look at what is actually signed, with sbverify --list.
An enrollment request still pending can be viewed with:
|
|
3. Install shim alongside rEFInd
We copy shim and MokManager into rEFInd’s directory on the ESP. Be careful to rename shim in the process: the Ubuntu package ships it under the name shimx64.efi.dualsigned.
mmx64.efi is MokManager, the blue screen from the previous step. It has to be in the same directory as shim, otherwise enrolling keys becomes impossible. Neither of them must be re-signed: shim is signed by Microsoft, MokManager by Canonical, and shim recognises the latter thanks to its embedded certificate.
Next we copy rEFInd under the name shim will look for:
|
|
This copy has to be redone after every rEFInd update — and signed, just like the original.
4. Sign the binaries
A note on method before the commands. You will often read:
Writing the output over the input file is risky: an interruption or an error along the way leaves a truncated binary, and therefore a machine that no longer boots. We sign to a temporary file, and only replace the original once the signature has succeeded. This small function does the job and, along the way, avoids stacking yet another signature onto an already valid file:
|
|
This safeguard is not cosmetic: sbsign appends a signature instead of replacing the existing one. A binary re-signed at every update ends up with a stack of identical signatures that grows indefinitely.
All that is left is to apply it to the right files:
|
|
If you have several kernels installed and you want to be able to select them in rEFInd, sign them all:
Verifying
sbverify --list displays the signatures present on a binary:
|
|
signature 1
image signature issuers:
- /CN=MyCustom
image signature certificates:
- subject: /CN=MyCustom
issuer: /CN=MyCustomAnd to validate a file against a specific key, which is more meaningful:
No KO should remain before re-enabling Secure Boot.
5. Create the boot entry
The firmware must now boot on shim, and no longer directly on rEFInd:
|
|
Adapt -d (the disk holding the ESP) and -p (the partition number) to your machine. To find them:
|
|
Then check that the new entry exists and comes first in BootOrder:
|
|
Do not delete the old entry pointing to refind_x64.efi right away: it remains a safety net as long as Secure Boot is disabled.
6. Automate for kernel updates
Every new kernel arrives unsigned. Without automation, the machine stops booting at the first update. So we put a hook in /etc/kernel/postinst.d/.
The filename matters: hooks are executed in alphabetical order. On Pop!_OS, zz-kernelstub, zz-systemd-boot and zz-update-grub already run at the end of the list and manipulate the boot files. So we prefix ours with zzz- to be sure to run last.
/etc/kernel/postinst.d/zzz-sign-secureboot:
|
|
|
|
The final exit 0 is deliberate: a hook that fails interrupts the configuration of the kernel package and leaves dpkg in a shaky state. Better a clearly visible error message and a system you can repair calmly.
We test it without waiting for the next update:
|
|
The output should contain the zzz-sign-secureboot: signed … lines, or nothing at all if everything was already signed.
7. Enable Secure Boot
All that is left is to re-enable Secure Boot in the firmware, making sure to boot on the shim entry. Once the system has started:
|
|
SecureBoot enabledAlso check that the kernel knows it booted in verified mode:
|
|
The kernel then enables lockdown mode: unsigned kexec, writing to /dev/mem and loading modules whose signature cannot be verified are all refused. That is the topic of the next section, and it is better to read it before rebooting.
8. Kernel modules: NVIDIA and the other DKMS ones
The symptom is disconcerting because it does not look like a boot failure. The system starts, the graphical session opens, everything looks normal — but only one screen lights up. On a machine with two GPUs, the ones wired to the NVIDIA card stay black, while the integrated GPU keeps driving the main display. Nothing in the interface reports anything at all.
The explanation lies in lockdown. The proprietary NVIDIA driver is not in the kernel: it is compiled on the machine by DKMS for every new kernel version. Under Secure Boot, a module whose signature does not chain back to a trusted key is simply refused.
The clean solution: a single key for everything
To have only one key to manage, we tell DKMS to use yours. Both variables are declared in /etc/dkms/framework.conf.d/, a directory that DKMS reads in addition to framework.conf:
/etc/dkms/framework.conf.d/secureboot.conf:
mok_signing_key="/root/secureboot-keys/MOK.key"
mok_certificate="/root/secureboot-keys/MOK.der"These values are evaluated before the per-distribution logic, so DKMS will no longer look in /var/lib/shim-signed/mok/. Do not delete that directory though: it remains your safety net if you ever want to roll back.
Modules that have already been built still carry the old signature. Since the signature is applied at build time, you have to force a rebuild:
|
|
Check the result without even rebooting: signer should now display the CN of your key.
|
|
This is where the codeSigning extension added in step 1 makes sense. If your certificate was generated without it, regenerate it — but be aware that this will force you to re-sign rEFInd, its drivers and the kernels, then enroll the key again. Hence the value of setting it from the start.
Once rebooted with Secure Boot enabled:
|
|
Conclusion
Your PC should now reboot in Secure Boot mode via rEFInd and take care of itself during kernel updates. This gives your PC an extra layer of protection.