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

  1. a LUKS encrypted volume on hugin with a detached header,
  2. a customized encrypt hook on hugin, which will accept a parameter naming a file containing a detached LUKS header,
  3. a Linux kernel and initramfs built on hugin and transferred to fulla, and
  4. a GRUB installation on fulla with a menu entry for booting hugin 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.