Enabling Secure Boot With REFInd and a Custom Key

Contents

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.

Diagram Code
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
flowchart TD
    FW["UEFI firmware<br/>db database: Microsoft + ASUS"]
    SHIM["shimx64.efi<br/><i>signed by Microsoft</i>"]
    MOK[("MOK<br/>our custom key")]
    REFIND["grubx64.efi = rEFInd<br/><i>signed with the MOK</i>"]
    DRIVER["drivers_x64/ext4_x64.efi<br/><i>signed with the MOK</i>"]
    KERNEL["/boot/vmlinuz<br/><i>signed with the MOK</i>"]
    WIN["Windows Boot Manager<br/><i>signed by Microsoft</i>"]

    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:

1
cat /proc/cmdline

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

1
sudo apt install sbsigntool mokutil shim-signed

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.

1
2
sudo mkdir -p /root/secureboot-keys
cd /root/secureboot-keys
1
2
3
4
sudo openssl req -new -x509 -newkey rsa:2048 \
  -keyout MOK.key -out MOK.crt -nodes -days 3650 \
  -addext "extendedKeyUsage=codeSigning" \
  -subj "/CN=PopOS Secure Boot/"

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:

1
2
sudo chmod 700 /root/secureboot-keys
sudo chmod 600 /root/secureboot-keys/MOK.key

mokutil expects a certificate in DER format, so we produce a conversion:

1
2
sudo openssl x509 -in /root/secureboot-keys/MOK.crt \
  -outform DER -out /root/secureboot-keys/MOK.der

We keep both formats: the .crt (PEM) for sbsign and sbverify, the .der for mokutil.

2. Enroll the key in the MOK

1
sudo mokutil --import /root/secureboot-keys/MOK.der

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:

  1. Enroll MOK
  2. Continue
  3. Yes
  4. enter the password chosen in the previous step
  5. 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:

1
mokutil --list-enrolled | grep -A1 Subject

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:

1
mokutil --list-new

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.

1
2
sudo cp /usr/lib/shim/shimx64.efi.dualsigned /boot/efi/EFI/refind/shimx64.efi
sudo cp /usr/lib/shim/mmx64.efi /boot/efi/EFI/refind/mmx64.efi

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:

1
sudo cp /boot/efi/EFI/refind/refind_x64.efi /boot/efi/EFI/refind/grubx64.efi

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:

1
2
# DO NOT DO THIS
sudo sbsign --key MOK.key --cert MOK.crt file.efi --output file.efi

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
sign_efi() {
    local f=$1
    local key=/root/secureboot-keys/MOK.key
    local crt=/root/secureboot-keys/MOK.crt

    [ -f "$f" ] || return 0
    if sbverify --cert "$crt" "$f" >/dev/null 2>&1; then
        echo "already signed: $f"
        return 0
    fi

    if sbsign --key "$key" --cert "$crt" --output "$f.tmp" "$f"; then
        chmod --reference="$f" "$f.tmp" 2>/dev/null
        mv -f "$f.tmp" "$f"
        echo "signed: $f"
    else
        rm -f "$f.tmp"
        echo "FAILED: $f" >&2
    fi
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# the kernel
sign_efi "/boot/vmlinuz-$(uname -r)"

# rEFInd, in both of its copies
sign_efi /boot/efi/EFI/refind/refind_x64.efi
sign_efi /boot/efi/EFI/refind/grubx64.efi

# the filesystem drivers — the step everyone forgets
for driver in /boot/efi/EFI/refind/drivers_x64/*.efi; do
    sign_efi "$driver"
done

If you have several kernels installed and you want to be able to select them in rEFInd, sign them all:

1
2
3
4
5
for k in /boot/vmlinuz-*; do
    case "$k" in *.signed|*.old) continue ;; esac
    [ -L "$k" ] && continue
    sign_efi "$k"
done

Verifying

sbverify --list displays the signatures present on a binary:

1
sudo sbverify --list /boot/efi/EFI/refind/grubx64.efi
signature 1
image signature issuers:
 - /CN=MyCustom
image signature certificates:
 - subject: /CN=MyCustom
   issuer:  /CN=MyCustom

And to validate a file against a specific key, which is more meaningful:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for f in /boot/vmlinuz-$(uname -r) \
         /boot/efi/EFI/refind/refind_x64.efi \
         /boot/efi/EFI/refind/grubx64.efi \
         /boot/efi/EFI/refind/drivers_x64/*.efi; do
    if sudo sbverify --cert /root/secureboot-keys/MOK.crt "$f" >/dev/null 2>&1; then
        echo "OK  $f"
    else
        echo "KO  $f"
    fi
done

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:

1
sudo efibootmgr -c -d /dev/nvme0n1 -p 1 -L "shim" -l '\EFI\refind\shimx64.efi'

Adapt -d (the disk holding the ESP) and -p (the partition number) to your machine. To find them:

1
findmnt /boot/efi

Then check that the new entry exists and comes first in BootOrder:

1
efibootmgr -v

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/bin/bash
# Secure Boot signing of the binaries loaded by rEFInd.
# Named zzz-* to run after zz-kernelstub, zz-systemd-boot and zz-update-grub.

KEY_DIR=/root/secureboot-keys
REFIND=/boot/efi/EFI/refind
VERSION=$1

[ -f "$KEY_DIR/MOK.key" ] && [ -f "$KEY_DIR/MOK.crt" ] || {
    echo "zzz-sign-secureboot: MOK keys missing from $KEY_DIR, nothing to do"
    exit 0
}

sign_if_needed() {
    local f=$1
    [ -f "$f" ] || return 0
    sbverify --cert "$KEY_DIR/MOK.crt" "$f" >/dev/null 2>&1 && return 0

    if sbsign --key "$KEY_DIR/MOK.key" --cert "$KEY_DIR/MOK.crt" \
              --output "$f.tmp" "$f" >/dev/null 2>&1; then
        chmod --reference="$f" "$f.tmp" 2>/dev/null
        mv -f "$f.tmp" "$f"
        echo "zzz-sign-secureboot: signed $f"
    else
        rm -f "$f.tmp"
        echo "zzz-sign-secureboot: FAILED to sign $f" >&2
    fi
}

shopt -s nullglob
sign_if_needed "/boot/vmlinuz-$VERSION"
sign_if_needed "$REFIND/refind_x64.efi"
sign_if_needed "$REFIND/grubx64.efi"
for driver in "$REFIND"/drivers_x64/*.efi; do
    sign_if_needed "$driver"
done

# Never make the kernel installation fail.
exit 0
1
sudo chmod 755 /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:

1
sudo apt install --reinstall linux-image-$(uname -r)

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:

1
mokutil --sb-state
SecureBoot enabled

Also check that the kernel knows it booted in verified mode:

1
dmesg | grep -i "secure boot"

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:

1
sudo apt install --reinstall linux-image-$(uname -r)

Check the result without even rebooting: signer should now display the CN of your key.

1
modinfo nvidia | grep signer

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:

1
lsmod | grep nvidia

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.

Contents