Security configurations for GRUB, a common Linux bootloader.
The steps to enable signature checking do not appear to be fully documented in GRUB's manual (at least as far as what's required in recent Ubuntu desktop releases), which is why this repository and the tool grub-mksignedboot.sh were created. This tool will do everything outlined in this README automatically, and can be run again to update signatures.
NOTE: grub-mksignedboot.sh has only been tested on:
- Ubuntu 20.04 (Desktop)
- Ubuntu 22.04 (Server)
- Kali 2022.3
- Some files under
/boot
will not be signed and GRUB will complain - These files do not need to be signed for Kali to boot
- Removing any other signatures will still prevent Kali from booting
- Some files under
This README covers two main points:
- Enforce signature checking in GRUB for the critical
/boot
components - Password protect the GRUB menu
In both cases this does not prevent tampering, the goal is to detect it.
Signing all of the /boot
files means any modifications to them will prevent the system from booting. This should serve as a warning to repeat and be familiar with these steps in a virtual machine or a test system before using them on production machines.
Password protecting both the UEFI/BIOS menu, and the GRUB menu requires a (non-targeted) attacker to open the case of the device, and remove your internal drives (mounting them directly to a system of their own) to modify the boot partitions. This can be detected by embedded UEFI/BIOS tamper protection mechanisms included by OEM's like Dell, or by sealing the screws on your case with paint or nail polish which won't prevent access but clearly shows if the case was opened. Thanks to Johannes Ullrich and the Daily StormCast as well as Jim Ducroiset from Active Countermeasuers for sharing the tip on using paint or nail polish!
This is an ideal situation. In less than ideal situations bypasses exist in less secure firmware which can allow attackers with physical access to enter the firmware's menu by connecting over external ports and sending a payload.
Ultimately this is meant to prevent non-targeted attacks where the adversary has physical access, from trivially backdooring your boot partition.
This does NOT prevent attackers with physical access from modifying your firmware, use a firmware password, TPM measurements, Measured Boot / vboot, or SecureBoot for this. Firmware is below the boot loader and the operating system.
For additional technical resources on firmware, boot, and lower level security, Paul Asadoorian's 3 part series on these topics is a great quick-start with examples:
- firmware-security-realizations-part-1-secure-boot-and-dbx
- firmware-security-realizations-part-2-start-your-management-engine
- firmware-security-realizations-part-3-spi-write-protections
- Create a GPG signing key just for GRUB files and
/boot
components - Save a non-ascii export of the public key to
/boot/grub/grub.pub
- Use
grub-mkstandalone
to compile a custom GRUB efi binary with signature checking modules and our public key embedded - Save this binary next to the current one, as
/boot/efi/EFI/ubuntu/grub_customx64.efi
- Sign all of the GRUB / boot components with the GPG key
- Password protect the GRUB menu, create a single administrative user
- Repeat the steps above to update GRUB and embed the admin user into the GRUB efi binary
NOTE: The path /boot/efi/EFI/ubuntu/
is used as this is the path on both 20.04 and 22.04. This path may change depending on the OS you're using.
Most if not all of these operations will require root. We'll use the /root
directory and root's GPG keyring, so only root has access.
Create the key:
- Enter "grub-signing-key" as the name
- Leave email blank
- For manually testing, use a simple password like 123456
sudo gpg --gen-key
Keep in mind root does not have the same gpg.conf
as you, and for signing GRUB and boot components, it's less important but worth considering if you need to make changes there.
Next we'll need to export the public key. This MUST be in a binary format (not --armor
) or GRUB will not be able to read it.
sudo gpg --export 'grub-signing-key' > /boot/grub/grub.pub
/boot/grub/grub.pub
was chosen because it's within the /boot
partition, making it available to read if needed for recovery. Otherwise this key can be read from anywhere by grub-mkstandalone
to embed it at compile time.
Update grub.cfg
so you can easily boot into a GRUB shell and review changes on the next reboot:
sudo sed -i 's/^GRUB_TIMEOUT=0$/#GRUB_TIMEOUT=0/' /etc/default/grub
sudo sed -i 's/^GRUB_TIMEOUT_STYLE=*$/GRUB_TIMEOUT_STYLE=countdown/' /etc/default/grub
You won't need to make any additional modifications to GRUB config files for signature verification. From the GRUB manual:
...Passing one or more
--pubkey
options togrub-mkimage
implicitly definescheck_signatures
equal toenforce
in core.img prior to processing any configuration files.
We never need to run grub-mkimage
directly, as it's run as part of grub-mkstandalone
.
Compiling a new efi binary with grub-mkstandalone
seems to be required to 'preload' the signature verification modules in GRUB to successfully perform signature validation.
- Simply running
sudo grub-install /dev/sda --pubkey /boot/grub/grub.pub
does not enforce signature checking. - Even running
sudo grub-mkimage ...
and replacing thecore.efi
binary under/boot/grub/x86_64-efi/
does not enforce signature checking.
As you'll see below, grub-mkstandalone
invokes grub-mkimage
. Using --verbose
to capture that command string, copy and pasting this command, and modifying it to execute still does not work. It appears to be missing the correct argument for --memdisk
which grub-mkstandalone
must generate in a temporary location at runtime.
You can verify this from a grub shell after trying the above steps for yourself by running:
list_trusted
echo $check_signatures
If you don't receive any output, the public key and the GRUB configuration are not loaded or embedded in the current grubx64.efi
or core.efi
image.
What we need to use is grub-mkstandalone
.
- We need to replace
/boot/efi/EFI/ubuntu/grubx64.efi
with our custom image that verifies signatures and knows our public key - During testing we'll write the custom image to
/boot/efi/EFI/ubuntu/grub_customx64.efi
so the original efi binary is still the default image to boot from - We'll attempt to boot the custom image from an EFI shell and address or note any issues from there
An easy way to test GRUB efi binaries is using a virtualization platform like VMware:
- Poweroff
- VMware > VM > Power > Power On to Firmware
- EFI Internal Shell
- Press any key to continue (before the countdown ends)
- Start below by typing
fs0:
and hitting enter, to enter the fs0 filesystem
fs0:
fs0:\> ls # just to show you can list contents of the .\EFI directory and more
fs0:\> .\EFI\ubuntu\grub_customx64.efi
The grub-mkstandalone
command is never mentioned in the GRUB manual, but is required (at least on Ubuntu) to successfuly enable and deploy the signature checking mechanism GRUB offers.
Thanks to this question posted by user Daniel, and answer provided by user Fonic. The grub-mkstandalone
command here was adapted directly from the examples in that post.
IMPORTANT: Check which version of GRUB you have installed, as of 2.06 the verifiers
module may no longer need preloaded (This could vary by system and needs tested).
grub-install --version
This is the command to create a new grubx64.efi
binary:
# Always update-grub first, in case there have been any changes
sudo update-grub
sudo grub-mkstandalone --verbose --format=x86_64-efi --output=/boot/efi/EFI/ubuntu/grub_customx64.efi --pubkey=/boot/grub/grub.pub --modules="verifiers gcry_sha256 gcry_sha512 gcry_dsa gcry_rsa" /boot/grub/grub.cfg=/boot/grub/grub.cfg
Because of --verbose
if we scroll up far enough in the ouput, the grub-mkimage
command that was run automatically is shown:
grub-mkimage --directory '/usr/lib/grub/x86_64-efi' --prefix '(memdisk)/boot/grub' --output '/boot/efi/EFI/ubuntu/grub_customx64.efi' --dtb '' --format 'x86_64-efi' --compression 'auto' --memdisk '/tmp/grub.fOZvyK' --pubkey '/boot/grub/grub.pub' 'verifiers' 'gcry_sha256' 'gcry_sha512' 'gcry_dsa' 'gcry_rsa' 'memdisk' 'tar'
After the command exits, the new efi binary will be under /boot/efi/EFI/ubuntu/grub_customx64.efi
This should always be the last step, once the GRUB binary has the public key and latest config file embedded you can sign everything to be sure all GRUB components will load.
Programmatically sign all boot files. The online manual for GRUB provides a script you can use to do this. The example below is a modified version of that script:
#!/bin/bash
# Must be run as root
if ! [[ "$EUID" == 0 ]]; then
echo -e "This script must be run as root."
exit 1
fi
# Ensure all changes made to GRUB's configuration are loaded
# This should be the last thing you do before signing all boot files
update-grub
# Compile the custom GRUB efi binary
grub-mkstandalone --verbose --format=x86_64-efi --output=/boot/efi/EFI/ubuntu/grub_customx64.efi --pubkey=/boot/grub/grub.pub --modules="verifiers gcry_sha256 gcry_sha512 gcry_dsa gcry_rsa" /boot/grub/grub.cfg=/boot/grub/grub.cfg
# This line is just for running tests
#echo '123456' > /dev/shm/passphrase.txt
# Write the GPG key's passphrase to memory for batch processing
if ! [ -e /dev/shm/passphrase.txt ]; then
echo "Paste your grub signing key passphrase into /dev/shm/passphrase.txt"
exit 1
fi
# Remove old signatures when updating
if (find /boot -type f -name "*.sig" -print0 | xargs -0 rm 2>/dev/null); then
echo "Removing previous signatures..."
fi
# Sign all of the GRUB / boot components
for i in $(find /boot -type f -name "*.cfg" -or -name "*.lst" -or -name "*.mod" -or -name "vmlinuz*" -or -name "initrd*" -or -name "grubenv"); do
if ! [ -e "$i".sig ]; then
echo "Signing $i..."
gpg --batch --detach-sign --pinentry-mode loopback --passphrase-fd 0 "$i" < /dev/shm/passphrase.txt
fi
done
echo "[>]Shredding plaintext key in memory..."
shred -n 7 -v /dev/shm/passphrase.txt
WARNING: Anytime you change or update GRUB, it's config, the kernel, or any other boot components, you need to recompile the custom GRUB binary and write new signatures for all the boot files.
Once the script is finished you can reboot / poweroff and continue to the next section.
A quick recap of what we've done up to now:
- Create a GPG signing key just for GRUB files and
/boot
components - Save a non-ascii export of the public key to
/boot/grub/grub.pub
- Use
grub-mkstandalone
to compile a custom GRUB efi binary with signature checking modules and our public key embedded - Save this binary next to the current one, as
/boot/efi/EFI/ubuntu/grub_customx64.efi
- Sign all of the GRUB / boot components with the GPG key
Because we still have the original grubx64.efi
binary in place, we can safely run the following command:
sudo rm /boot/efi/EFI/ubuntu/*.sig
This will delete all of the detached signature files in that directory.
Go through the steps once more of booting into the EFI shell and loading our custom GRUB binary:
fs0:
fs0:\> EFI/ubuntu/grub_customx64.efi
You should see GRUB complain that a .sig
file was not found. Just one failed signature is enough to prevent GRUB from booting into the OS.
To correct this, reboot so the original grubx64.efi
binary (which is still the default) loads, and sign all of the GRUB components.
Run the following to 'install' the custom GRUB binary, and keep a backup of the original:
sudo cp -n /boot/efi/EFI/ubuntu/grubx64.efi /boot/efi/EFI/ubuntu/grubx64.bkup.efi
sudo mv /boot/efi/EFI/ubuntu/grub_customx64.efi /boot/efi/EFI/ubuntu/grubx64.efi
This is a good way to test this on a small set of systems.
- The GRUB menu is (or will be in the next section) password protected
- If something goes wrong, boot into an EFI shell and execute the previous image:
grubx64.bkup.efi
GRUB - Authentication and authorisation
This is done last so you aren't stuck entering a GRUB password while testing and configuring signature verification.
Generate a hash of your password:
grub-mkpasswd-pbkdf2
The hash is the entire string, starting with grub.pbkdf2.sha512.10000...
, which we'll need to highlight and copy.
This creates a single administrative user and password protects all GRUB menu entries.
Enter the following lines (with your own password hash) below the existing comments at the top of /etc/grub.d/40_custom
:
set superusers="admin"
password_pbkdf2 admin grub.pbkdf2.sha512.10000.4DC37841103E9C41817DB083383337...
This will require a password to move past the GRUB menu, meaning even just to boot into the OS. This can be improved by allowing anyone to boot into the installed OS, but only the local administrator can modify GRUB or access the other menu items such as the firmware menu.
Finally, after adding your administrative user to/etc/grub.d/40_custom
, you'll need to update GRUB and compile this new configuration into the custom efi binary:
sudo update-grub
sudo grub-mkstandalone --format=x86_64-efi --output=/boot/efi/EFI/ubuntu/grub_customx64.efi --pubkey=/boot/grub/grub.pub --modules="verifiers gcry_sha256 gcry_sha512 gcry_dsa gcry_rsa" /boot/grub/grub.cfg=/boot/grub/grub.cfg
- Sign all of the GRUB components again
On the next boot (using the custom efi binary) you'll be taken to the GRUB menu and asked for a password before any action can be taken.