Project Fulla: Crypto utility USB key - Part 2: Crypto setup
In the last post I showed how to set up an encrypted -
including /boot
- Arch Linux system on a USB drive. In this post I will show
how to also set up this same USB drive for use as a “keyring” holding detached
LUKS headers and boot files for other machines.
This approach provides a kind of two-factor authentication for unlocking the target machines, since you need both the LUKS header (owned factor) and the passphrase (known factor) to unlock and boot it. This scenario is documented well on the Arch wiki. The extra steps provided by this guide will enable you to use the same “keyring” USB drive and GRUB installation for multiple target systems.
The target system, which will be the one encrypted with a detached LUKS header,
in this scenario will be called hugin
, and the keyring system will be called
fulla
. All shell commands will be run on hugin
, and I’ll assume you have the
boot volume from fulla
mounted at /boot
.
Components
The components we need to make this work are
- a LUKS encrypted volume on
hugin
with a detached header, - a customized
encrypt
hook onhugin
, which will accept a parameter naming a file containing a detached LUKS header, - a Linux kernel and initramfs built on
hugin
and transferred tofulla
, and - a GRUB installation on
fulla
with a menu entry for bootinghugin
with its kernel and initramfs
(Technically, we could probably do with just the one kernel which we reuse for all target systems, but that would require keeping all target systems updated in sync to avoid breakage. Keeping the kernel and initramfs images separate between systems will make the setup a lot more robust.)
The next few sections explain how to set up each of the above components.
Step 1: Detached LUKS header
When setting up LUKS on hugin
, we save the LUKS header detached to a file
instead of on the drive.
# cryptsetup luksFormat /dev/sdX --header /boot/header-hugin.img
Note that this must be done at the time of the encryption setup, and cannot be retrofitted on later. If you want to do this with a system already encrypted with LUKS with an on-drive header, you’ll need to backup the filesystem contents, wipe the drive, set up encryption and filesystems again, and restore the filesystem contents from your backup.
If you hadn’t already installed Linux on hugin
, do so now and come back here
when you’re done.
Step 2: Customized encrypt
hook
The encrypt
mkinitcpio hook shipped with Arch Linux at the time of this
writing isn’t prepared to handle detached LUKS headers. Therefore we’ll need to
modify the encrypt
hook slightly. The procedure is taken from the Arch
Wiki, and is presented in two versions in the next
two subsections: the AUR package version and the DIY version. Whichever version
you choose, the end result will be that we can pass a header=<path>
argument
to the cryptdevice
kernel parameter. The header=<path>
value will be used as
the path to the file containing the LUKS header. This is done in
/etc/default/grub
as described in the next section.
Option 1: AUR package
To save you the trouble, I’ve packaged the
steps below as initcpio-encrypt-remote-luks-header on the
AUR for you Arch users. The package contains the encrypt2
hook as described
in the next section, but named encrypt_remote_luks_header
instead.
Option 2: DIY
This is what the AUR package would do for you.
We’ll leave the default encrypt
hook in place and modify a copy of it:
# cp /lib/initcpio/hooks/encrypt{,2}
# cp /usr/lib/initcpio/install/encrypt{,2}
The diff of our changes looks like this:
$ git diff --no-index /lib/initcpio/hooks/encrypt{,2}
diff --git a/lib/initcpio/hooks/encrypt b/lib/initcpio/hooks/encrypt2
index 819c4cf..77fc562 100644
--- a/lib/initcpio/hooks/encrypt
+++ b/lib/initcpio/hooks/encrypt2
@@ -49,11 +49,17 @@ EOF
echo "Use 'cryptdevice=${root}:root root=/dev/mapper/root' instead."
}
+ local headerFlag=false
for cryptopt in ${cryptoptions//,/ }; do
case ${cryptopt} in
allow-discards)
cryptargs="${cryptargs} --allow-discards"
;;
+ header=*)
+ cryptargs="${cryptargs} --header ${cryptopt#header=}"
+ headerFlag=true
+ echo "Using detached header ${cryptopt#header=}"
+ ;;
*)
echo "Encryption option '${cryptopt}' not known, ignoring." >&2
;;
@@ -61,7 +67,7 @@ EOF
done
if resolved=$(resolve_device "${cryptdev}" ${rootdelay}); then
- if cryptsetup isLuks ${resolved} >/dev/null 2>&1; then
+ if $headerFlag || cryptsetup isLuks ${resolved} >/dev/null 2>&1; then
[ ${DEPRECATED_CRYPT} -eq 1 ] && warn_deprecated
dopassphrase=1
# If keyfile exists, try to use that
initramfs configuration
With our customised encrypt2
hook (or encrypt_remote_luks_header
, if using
the AUR package) in place, we need to configure our initramfs to use it. We need
the encrypt2
hook in HOOKS
and the header file in FILES
:
FILES="/boot/header-hugin.img"
HOOKS="base udev autodetect modconf block encrypt2 lvm2 filesystems keyboard fsck"
Step 3: Kernel and initramfs images
Since we want to boot multiple different machines from the same /boot
, we’ll
need to keep their kernel and initramfs images apart. An easy way to do this is
to simply copy them to different filenames after building them. This way,
whenever pacman updates the kernel (for Arch users) and overwrites
/boot/vmlinuz-linux
and rebuilds /boot/initramfs-linux.img
, the images we
actually use are safely untouched. We can then copy the new files to their safe
filenames and be on our merry way. For now we’ll do it manually, but the next
post will explain how to automate the procedure.
So, this step is in fact quite simple. We’ll just (re)install the linux
package, triggering an overwrite of the kernel at /boot/vmlinuz-linux
and a
rebuild of the initramfs at /boot/initramfs-linux.img
. Non-Arch users may need
to adapt this step to your own kernel/initramfs build procedure, or just skip
ahead to copying files if your distribution never updates these files. First of
all, though, we’ll need to copy the existing /boot/vmlinuz-linux
and
/boot/initramfs-linux.img
files belonging to the fulla
system.
# cp /boot/vmlinuz-linux{,-fulla}
# cp /boot/initramfs-linux{,-fulla}.img
# pacman -S linux
# cp /boot/vmlinuz-linux{,-hugin}
# cp /boot/initramfs-linux{,-hugin}.img
Step 4: GRUB configuration
This is where things get messy. Since we want to use the same bootloader for all of the systems serviced by the USB key - so we can pick the system to boot from a nice GRUB menu rather than having to pick different partitions in BIOS - we’re going to need to merge all of them together by hand.
The cryptdevice
kernel parameter
Since we’re using a detached LUKS header, the encrypted partition on hugin
is
just a plain dm-crypt
encrypted volume with no cleartext metadata header. As
such, it doesn’t have a UUID either, so we’ll have to resort to other ways of
identifying the block device. In my case, I can use a device identifier in
/dev/disk/by-id
:
GRUB_CMDLINE_LINUX="cryptdevice=/dev/disk/by-id/ata-ST9160310AS_5SV4DJDE-part1:lvm:header=/boot/header-hugin.img
Optional: LUKS keyfile for auto-unlocking after bootloader
For a bit of extra convenience, we can also set up a keyfile in our
encrypted/boot
to use for unlocking the filesystems on hugin
. This will save
us from having to enter another passphrase to boot hugin
after entering the
first to unlock the bootloader, effectively turning the USB key into a “master
key” for all the systems you do this for. If this sounds like a bit too many
eggs in the same basket, feel free to skip ahead to the next subsection.
The keyfile setup is easy: we create a keyfile, register it as a LUKS key, add
it to the initramfs image and set the cryptkey
kernel parameter. This was
covered in the previous post, but I’ll duplicate it
here for good measure.
# dd bs=512 count=4 if=/dev/urandom of=/boot/keyfile-hugin.bin
# cryptsetup luksAddKey /dev/sde2 /boot/keyfile-hugin.bin
Add the file to FILES
in /etc/mkinitcpio.conf
…
FILES="/boot/header-hugin.img /boot/keyfile-hugin.bin"
…and rebuild the initramfs:
# mkinitcpio -p linux
# cp /boot/initramfs-linux{,-hugin}.img
Now we just need to set the cryptkey
kernel parameter in /etc/default/grub
:
GRUB_CMDLINE_LINUX="cryptdevice=/dev/disk/by-id/ata-ST9160310AS_5SV4DJDE-part1:lvm:header=/boot/header-hugin.img cryptkey=rootfs:/boot/keyfile-hugin.bin"
Generating and merging GRUB configuration
Now we can generate our GRUB configuration. However, we’re not going to write it
to /boot/grub/grub.cfg
, because we need to merge our menu entry into the
existing GRUB configuration rather than overwriting the configuration we’ve set
up for the other machines.
In case you skipped setting up a Linux system on the USB key, you may not yet
have a /boot/grub/grub.cfg
. In that case, just unlock and mount /boot
and
write the GRUB configuraiton there, and skip ahead to the next subsection. Then
return here when going through the setup for next machine.
# grub-mkconfig -o /tmp/grub.cfg
Found linux image: /boot/vmlinuz-linux-hugin
Found initrd image: /boot/initramfs-linux-hugin.img
Found linux image: /boot/vmlinuz-linux
Found initrd image: /boot/initramfs-linux.img
Found fallback initrd image: /boot/initramfs-linux-fallback.img
Now for the messy part: merging the GRUB menu entry from /tmp/grub.cfg
into
/boot/grub/grub.cfg
. It’s not so bad, though; what we need to do is locate the
main menu entry in /tmp/grub.cfg
and copy it into /boot/grub/grub.cfg
. In my
case, after copying the kernel and initramfs images as above the interesting
part of /tmp/grub.cfg
looks like this:
### BEGIN /etc/grub.d/10_linux ###
menuentry 'Arch Linux' --class arch --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-/dev/mapper/hugin--hdd-root' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod cryptodisk luks gcry_rijndael gcry_rijndael gcry_sha256 lvm
insmod ext2
set root='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1' 5093d5b7-9407-4071-b724-fb45a9f3a2cc
else
search --no-floppy --fs-uuid --set=root 5093d5b7-9407-4071-b724-fb45a9f3a2cc
fi
echo 'Loading Linux linux-hugin ...'
linux /vmlinuz-linux-hugin root=/dev/mapper/hugin--hdd-root rw cryptdevice=/dev/disk/by-id/ata-ST9160310AS_5SV4DJDE-part1:lvm:header=/boot/header-hugin.img cryptkey=rootfs:/boot/keyfile-hugin.bin root=/dev/mapper/hugin--hdd-root resume=/dev/mapper/hugin--hdd-swap quiet
echo 'Loading initial ramdisk ...'
initrd /initramfs-linux-hugin.img
}
And the menu entry for the embedded Linux system set up in the previous post looks like this:
menuentry 'Arch Linux' --class arch --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-3fde947e-fe67-42f9-bc07-6a6b615bbba5' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod cryptodisk luks gcry_rijndael gcry_rijndael gcry_sha256 lvm
insmod ext2
cryptomount -u 2f077bf5730f483ea2666321fb398cfb
set root='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1' 5093d5b7-9407-4071-b724-fb45a9f3a2cc
else
search --no-floppy --fs-uuid --set=root 5093d5b7-9407-4071-b724-fb45a9f3a2cc
fi
echo 'Loading Linux linux ...'
linux /vmlinuz-linux root=UUID=3fde947e-fe67-42f9-bc07-6a6b615bbba5 rw cryptdevice=UUID=2f077bf5-730f-483e-a266-6321fb398cfb:lvm cryptkey=rootfs:/boot/keyfile-fulla.bin quiet
echo 'Loading initial ramdisk ...'
initrd /initramfs-linux.img
}
Looks similar enough, so let’s just copy the former in after the latter and
modify the output strings to distinguish between the menu entries. You’ll also
notice that the fulla
menu entry still uses the default vmlinuz-linux
and
initramfs-linux.img
file names, so let’s update those too. After these
changes, /boot/grub/grub.cfg
looks like this:
menuentry 'Arch Linux on fulla' --class arch --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-3fde947e-fe67-42f9-bc07-6a6b615bbba5' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod cryptodisk luks gcry_rijndael gcry_rijndael gcry_sha256 lvm
insmod ext2
cryptomount -u 2f077bf5730f483ea2666321fb398cfb
set root='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1' 5093d5b7-9407-4071-b724-fb45a9f3a2cc
else
search --no-floppy --fs-uuid --set=root 5093d5b7-9407-4071-b724-fb45a9f3a2cc
fi
echo 'Loading Linux linux ...'
linux /vmlinuz-linux-fulla root=UUID=3fde947e-fe67-42f9-bc07-6a6b615bbba5 rw cryptdevice=UUID=2f077bf5-730f-483e-a266-6321fb398cfb:lvm cryptkey=rootfs:/boot/keyfile-fulla.bin quiet
echo 'Loading initial ramdisk ...'
initrd /initramfs-linux-fulla.img
}
menuentry 'Arch Linux on hugin' --class arch --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-/dev/mapper/hugin--hdd-root' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod cryptodisk luks gcry_rijndael gcry_rijndael gcry_sha256 lvm
insmod ext2
set root='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint='lvmid/DWO4DE-dN4i-PXC7-Ru8W-6tmE-mkVp-iq1kSJ/IYKjtn-A2UC-wPmF-N619-pfuc-Ms0o-0HE9k1' 5093d5b7-9407-4071-b724-fb45a9f3a2cc
else
search --no-floppy --fs-uuid --set=root 5093d5b7-9407-4071-b724-fb45a9f3a2cc
fi
echo 'Loading Linux linux-hugin ...'
linux /vmlinuz-linux-hugin root=/dev/mapper/hugin--hdd-root rw cryptdevice=/dev/disk/by-id/ata-ST9160310AS_5SV4DJDE-part1:lvm:header=/boot/header-hugin.img cryptkey=rootfs:/boot/keyfile-hugin.bin root=/dev/mapper/hugin--hdd-root resume=/dev/mapper/hugin--hdd-swap quiet
echo 'Loading initial ramdisk ...'
initrd /initramfs-linux-hugin.img
}
And with that, you should be all set! Unless I forgot something when writing this up, that should be all you need to boot from your shiny new USB key. Try it out and let me know if you have any issues or ideas for improvements!
Extras: Automation
While this is all well and good, you’ll have noticed there’s an ugly recurring
manual step in here: Whenever the kernel or initramfs image changes - that is
every linux
package upgrade on Arch Linux - you’ll need to copy the files over
to their safe names. Luckily, if you’re using Arch Linux, you can hook into
pacman and have this done automatically after every package upgrade. Other
package managers may have similar functionality as well. Anyway, this will be
covered in the last installment in this series of posts.