Introduction
Why ? You may ask. If like me, you like to work on your laptop from different places, then you may want to make sure that your sensitives data stay protected in case someone as a physical access to your computer.
I'm not an expert in computer security. So please, be aware that my solution is not perfect and many vulnerabilities could still be exploited. Also, there's many different way to achieve this but I'll focus on only one method to avoid confusion.
I have a 5 years old Dell XPS 15 and I'm using Arch Linux. It should works with another GNU/Linux distribution as the features used are mainly kernel related and the other packages are pretty common. However, the computer need to support EFI secure boot and being able to lock the BIOS setup interface behind a password.
Credits and sources
- ArchLinux Wiki: Installation guide
- ArchLinux Wiki: Dm-crypt/LUKS on LVM
- ArchLinux Wiki: Unified Extensible Firmware Interface/Secure Boot
- Matthew Bently: Combine the kernel, initramfs, and boot options
Root encryption
Don't forget to verify the signature of your installation image as to avoid any malware from the start, then proceed with the installation normally until you need to prepare your disk.
Create a new partition table
Using fdisk, look for the name of your disk.
fdisk -l
In my case, having a NVMe SSD, my disk is named nvme0n1 but otherwise, it's commonly named sda.
Name | Type | Size |
---|---|---|
/dev/sda1 | EFI | 512MB |
/dev/sda2 | Linux filesystem | Remainder of the device |
That's how the partition table will look like if you follow my example. Normally, I like to use LVM as it offers more flexibility but I removed it to simplify. Also, I don't have a swap partition.
Start fdisk.
fdisk /dev/sda
Inside the prompt:
- Type g to create a new empty GPT partition table.
- Type n to add a first partition.
- Type Enter a first time to accept the default partition number, then Enter a second time to accept the default first sector and finally +512M for the last sector.
- Type t then efi to change the type to 'EFI system'.
- Type n to add a second partition.
- Type Enter 3 times to accept all default values.
- Type w to save the changes and exit.
Setup LUKS
I chose to detach the header into a USB key as a simple way to get a physical authentication factor. If your passphrase somehow leaked, your partition cannot be decrypted without the salt contained in the header. Also, even if someone had the computational power to break the encryption, it would make the process a bit harder.
mount /dev/sdb1 /mnt # mount the usb
cryptsetup luksFormat /dev/sda2 --header /mnt/header.img
cryptsetup open /dev/sda2 root --header /mnt/header.img
umount /mnt
I strongly recommend making a backup of the header file somewhere safe. You cannot decrypt the root partition without it. So if your USB key fails, you'll lose all your data.
Format both partitions
mkfs.vfat -F32 /dev/sda1
mkfs.ext4 /dev/mapper/root
Overwrite previous data
This is a very long process depending of the size of your partition. If you don't do this, some old data could still be potentially retrieved.
dd if=/dev/zero of=/dev/mapper/root status=progress
No need to use /dev/random
because the encryption already turn the zeroes into random data.
Proceed with the rest of the installation and chroot into your new installation. Don't install any bootloader, the kernel will boot directly from the firmware.
Decrypt partition at boot
In order to decrypt your root partition at boot with a detached LUKS header on a usb key, add a custom hook to initcpio.
I chose to use labels instead of GUIDs so I can use a backup USB key if the main one fails. So be aware that if there's another external drive connected to your laptop while booting, you may not be able to decrypt your root partition because of that.
/etc/initcpio/hooks/customencrypt
#!/usr/bin/ash
run_hook() {
modprobe -a -q dm-crypt >/dev/null 2>&1
while [ ! -b '/dev/sdb1' ]; do
sleep 1
done
mkdir -p /mnt
mount /dev/sdb1 /mnt
cryptsetup --header /mnt/header.img open /dev/sda2 root
umount /mnt
}
/etc/initcpio/install/customencrypt
#!/bin/bash
build() {
local mod
add_module "dm-crypt"
add_module "dm-integrity"
if [[ $CRYPTO_MODULES ]]; then
for mod in $CRYPTO_MODULES; do
add_module "$mod"
done
else
add_all_modules "/crypto/"
fi
add_binary "cryptsetup"
add_binary "dmsetup"
add_file "/usr/lib/udev/rules.d/10-dm.rules"
add_file "/usr/lib/udev/rules.d/13-dm-disk.rules"
add_file "/usr/lib/udev/rules.d/95-dm-notify.rules"
add_file "/usr/lib/initcpio/udev/11-dm-initramfs.rules" "/usr/lib/udev/rules.d/11-dm-initramfs.rules"
# cryptsetup calls pthread_create(), which dlopen()s libgcc_s.so.1
add_binary "/usr/lib/libgcc_s.so.1"
add_runscript
}
And finally, modify your mkinitcpio configuration file to add the custom module.
/etc/mkinitcpio.conf
HOOKS=(base udev autodetect keyboard keymap modconf block filesystems fsck customencrypt)
Use this mkinitcpio command to create your new initial ramdisk image.
mkinitcpio -P
Secure boot
Why going through the hassle of enabling secure boot ? Well, your root partition is safe but your kernel is not. Imagine you let your laptop unsupervised a few minutes, someone could boot from a usb and replace your kernel with a custom one without you noticing. It's something you really don't want and I don't really need to explain why.
Install the sbsigntools package.
pacman -S sbsigntools
Create a new directory in a secure location like /root/secureboot
.
mkdir /root/secureboot
cd /root/secureboot
Generate your own keys
Create a script named mkkeys.sh
that will automate the process.
#!/bin/bash
echo -n "Enter a Common Name to embed in the keys: "
read name
#GUID
uuidgen --random > GUID.txt
#PLATFORM KEY
openssl req -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj "/CN=$name PK/" -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
sign-efi-sig-list -g "$(< GUID.txt)" -c PK.crt -k PK.key PK /dev/null rm_PK.auth
#KEY EXCHANGE KEY
openssl req -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj "/CN=$name KEK/" -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=$name db/" -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
Then execute the script.
touch mkkeys.sh
chmod +x mkkeys.sh
./mkkeys.sh
Generate signed unified kernel
Create a file named cmdline.txt
containing the kernel parameters.
BOOT_IMAGE=/vmlinuz-linux initrd=/intel-ucode.img initrd=/initramfs-linux.img root=/dev/mapper/root
Notice that I'm using the Intel microcode. If you don't have a Intel CPU or just don't want to use it, some modifications are required.
Create a script named mkkernel.sh
.
#!/bin/sh
cd /root/secureboot
#COMBINE FILES
cat /boot/intel-ucode.img /boot/initramfs-linux.img > initramfs-linux.img
cp /boot/vmlinuz-linux .
objcopy \
--add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000 \
--add-section .cmdline="cmdline.txt" --change-section-vma .cmdline=0x30000 \
--add-section .linux="vmlinuz-linux" --change-section-vma .linux=0x40000 \
--add-section .initrd="initramfs-linux.img" --change-section-vma .initrd=0x3000000 \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub kernel.efi
#SIGN EFI
sbsign --key db.key --cert db.crt --output kernel.efi.signed kernel.efi
#COPY KERNEL
cp kernel.efi.signed /boot/kernel.efi
Make it executable.
touch mkkernel.sh
chmod +x mkkernel.sh
You can manually execute the script each time you want to update the kernel but I recommend to automate the process with a pacman hook.
Create a file at /etc/pacman.d/hooks/91-secureboot.hook
.
[Trigger]
Type = Path
Operation = Install
Operation = Upgrade
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*
[Action]
Description = Making signed kernel UEFI
When = PostTransaction
Exec = /root/secureboot/mkkernel.sh
NeedsTargets
Enroll keys in firmware
I tried to use sbkeysync but despite being in setup mode, I was unable to enroll my keys so I had to enroll them manually, using my BIOS setup interface. Don't forget to enable secure boot mode in your BIOS setup interface and to lock it behind a password because if someone is able to simply turn it off, it would make all this pointless.
Bootloader
I did not install any bootloader because it's simply useless in this case. Instead go into your BIOS setup interface and make a new boot entry for kernel.efi
.
Recommendations
- I like to use a passphrase instead of password. It may be a bit longer to enter each time but it's easier to remember and save you the trouble of having to distinguish similar letters. Also a 8 word passphrase from a dictionary of 3600 words is stronger than a 8 long password from 60 different characters (3600^8 > 60^8).
- Use a idle screen locker and always lock your laptop before leaving it.
- Be aware of who might be looking at your screen/keyboard while you enter your passphrase.
- Don't let the usb key containing the LUKS header on your laptop. Remove it after the boot process and keep it with you.