Full-disk encryption on Slackware, the modern way

it's 2022; who needs a separate /boot partition?

As I mentioned in a previous post, it’s possible to bake a LUKS key into Slackware’s initial ramdisk and have init automatically unlock a LUKS-encrypted LVM physical volume (and volume group, and logical volumes) on boot, such that when using GRUB’s LUKS/LVM support you only have to enter your passphrase once instead of twice.

…what?

So pretty much every reasonably-modern (GNU/)Linux distribution (and I use that term “reasonably-modern” very loosely, i.e. “released within the last decade or so”) supports using LUKS (Linux Unified Key Setup) and LVM (Logical Volume Manager) to implement full-disk encryption - that is, all (or nearly all) of the data on the computer’s hard drive is encrypted with at least one passphrase or decryption key, similar to how you’d use BitLocker or FileVault on Windows or macOS (respectively). In short, the usual way of going about full disk encryption in the Linux world would be something like the following:

  1. Create a giant partition on your chosen disk to hold all the data (we’ll call it $PARTITION; usually this would be something like sda1 or nvme0n1p2 or somesuch)

  2. Optionally run dd if=/dev/urandom of=/dev/$PARTITION to fill that partition with random data (helps hide the encrypted data, making it a little bit harder for someone with oodles and oodles of computing power - like, say, a particular agency of national security - to crack it)

  3. Run cryptsetup luksFormat /dev/$PARTITION and cryptsetup luksOpen /dev/$PARTITION luks$PARTITION to turn the partition into an empty encrypted virtual disk and open it for reading/writing

  4. Run pvcreate /dev/mapper/luks$PARTITION and vgcreate $LVM_VG /dev/mapper/luks$PARTITION (where $LVM_VG is some name; openSUSE uses system, Slackware suggests cryptvg, but it can be whatever the user wants) to create an LVM physical volume and volume group

  5. Run various incantations of lvcreate -L ${SIZE}G -n $NAME $LVM_VG for various virtual partitions (at minimum / (root) and swap; usually also /home) and format them (with mkfs.* or mkswap) as appropriate

  6. ???

  7. Profit

Traditionally, in addition to that encrypted data partition, you’d also need a separate unencrypted partition to hold the “real” (a.k.a. second-stage) bootloader (usually GRUB), its configuration files, and the Linux kernel (vmlinuz) and accompanying ramdisk (initrd or initramfs) - this extra partition usually being mounted at /boot. However, in this day and age, we don’t need (and quite possibly don’t want) that extra partition anymore, and some newer distros (like recent versions of openSUSE) do away with that extra partition, instead using GRUB’s built-in support for LUKS and LVM.

Why?

It might seem like a pointless exercise to eliminate the /boot partition. No harm in keeping it around, right? Well, that depends:

By eliminating the separate /boot partition:

This of course only mitigates the so-called “Evil Maid” attack (wherein someone with physical access to your machine tampers with it), and does nothing against, say, some totally-safe program you decided to download off the World Wide Web and run as root, but nonetheless, if you don’t need a separate /boot partition, then it seems kinda silly to keep one around, right?

Why not?

For one, Slackware doesn’t really support this out-of-the-box; it’ll work well enough to be usable, but you’ll have to enter your passphrase twice on every boot unless you’re willing to hack Slackware’s initrd.gz a bit (which I’ll obviously cover below).

Also, GRUB’s filesystem support doesn’t exactly match Linux’s filesystem support. If you’re using something other than a very plain ext4 for your root filesystem… well, expect trouble. There are countless horror stories online of GRUB refusing to recognize a filesystem it ostensibly supports because of some discrepancy between the GRUB v. Linux implementations, and I ran into snags even with ext4 by enabling extra features (namely: casefolding) that GRUB simply doesn’t support. GRUB is, put simply, surprisingly picky about things, and this will mean constraining your root filesystem to appease that pickiness; if you want to do something especially fancy for your root FS, you’ll probably want to stick with the separate /boot partition.

Enough with the pros and cons, though. Let’s get to actually doing it!

How?

Like I mentioned above, Slackware doesn’t normally support a /bootless full-disk encryption scheme; the instructions provided on the installation media insist on creating a separate partition for /boot (and also is pretty silent on UEFI; you’re expected to cross-reference the separate instructions to that effect). Luckily, the install environment includes everything we need to pull this off anyway, though we’re gonna have to deviate from those instructions a bit.

High level summary of the deviations:

Partitioning

Ain’t a whole lot here that’s different from what README_CRYPT.TXT tells you to do; only notable difference is the lack of a /boot partition (and the inclusion of a partition for /boot/efi instead). Make sure you know what disk you’re using; in my case it’s nvme0n1 (because I only have one drive and it’s NVMe), but it could be sda (if you have a single SATA or SAS - or, God forbid, IDE or SCSI - disk) or nvme0n2 or sdb (if you have multiple disks) or mmcblk1 (if you’re installing to an SD card) or something even crazier.

DISK=nvme0n1
EFI_PARTITION=nvme0n1p1
DATA_PARTITION=nvme0n1p2
SWAP_SIZE=32  # GB; double your RAM
ROOT_SIZE=100 # GB
HOME_SIZE=250 # GB; optional
dd if=/dev/urandom of=/dev/$DISK  # optional
cfdisk /dev/$DISK  # GPT, 512MB "EFI System", rest "Linux LVM"
mkfs.fat /dev/$EFI_PARTITION
cryptsetup luksFormat /dev/$DATA_PARTITION
cryptsetup luksOpen /dev/$DATA_PARTITION luks$DATA_PARTITION
vgcreate cryptvg /dev/mapper/luks$DATA_PARTITION
lvcreate -L ${SWAP_SIZE}G -n swap cryptvg
lvcreate -L ${ROOT_SIZE}G -n root cryptvg
lvcreate -L ${HOME_SIZE}G -n home cryptvg # optional
mkswap /dev/cryptvg/swap

At this point, you can format your root (and home, if you created it) partitions as well, or you can wait and do that via the installer. Like I mentioned above, your root filesystem needs to appease GRUB’s pickiness, so if you’re going to deviate from something tried-and-true (like mkfs.ext4 /dev/cryptvg/root) you should probably do some investigation first - or else you’ll end up like I did, banging my head on the keyboard because I found out after going through the whole install process that GRUB will flat out refuse to recognize an ext4 filesystem if it so much as supports casefolding.

Installing

With your partitions squared away, you can run through Slackware’s usual installation process as README_CRYPT.TXT instructs (i.e. run setup, pick ADDSWAP, and away we go). The only real deviation here (other than the lack of a partition for /boot) is that you want to skip both LILO and ELILO, since neither supports LVM or LUKS and we don’t want anything interfering with GRUB (grub-install shouldn’t care about any existing (E)LILO installation, but better safe than sorry).

I also strongly suggest you skip installing the kernel-huge package (i.e. by picking either menu/expert or newbie when prompted, and unchecking the kernel-huge package in the A series). There’s no use in having it around, since kernel-generic is the only one that’ll be able to actually boot our system, and if it’s present then grub-mkconfig will rather dumbly make it the default boot option, so it’s arguably best to get rid of it (rather than deal with the trouble of hacking on GRUB configuration generators for the sake of a kernel you’ll absolutely never want or need).

Finishing up

Before we reboot, we need to tie up some loose ends. This is the part where we deviate the most from README_CRYPT.TXT, and is also the part that could get kinda hairy.

Install GRUB

First, let’s chroot into our new Slackware installation and get GRUB squared away:

chroot /mnt
DISK=nvme0n1
DATA_PARTITION=nvme0n1p2
echo "GRUB_ENABLE_CRYPTODISK=y" >> /etc/default/grub
echo "GRUB_CMDLINE_LINUX=\"cryptdevice=/dev/${DATA_PARTITION}:lvm\"" >> /etc/default/grub
grub-mkconfig -o /boot/grub/grub.cfg
grub-install /dev/$DISK

If grub-mkconfig and/or grub-install complain about an unrecognized filesystem, then congrats: you’ve picked a root filesystem (or options/features thereof) that GRUB doesn’t like, and you’ll have to reformat /dev/cryptvg/root, go through setup again, and hope you made a better choice this time. You will not pass Go, you will not collect $200, and you certainly will not get much further than GRUB’s rescue prompt if you reboot at this point. I learned this the hard way by doing mkfs.ext4 -O casefolding,64bit /dev/mapper/root and wondering why GRUB hated me.

So, hopefully you didn’t get such a complaint, in which case it’s time for some initrd hackery.

Hack initrd.gz

This part is technically optional; all it does is save you from having to type your passphrase twice. It’s also the most complicated, because we have to patch Slackware’s init script within the initial ramdisk. Here Be Dragons™.

First, a couple commands:

dd bs=512 count=4 if=/dev/urandom of=/boot/cryptkey.bin
chmod 600 /boot/cryptkey.bin
cryptsetup luksAddKey /dev/$DATA_PARTITION /boot/cryptkey.bin
echo 'LUKSKEY="/cryptkey.bin"' >> /etc/mkinitrd.conf

Now for the rocket surgery: Slackware’s default init assumes that the LUKSKEY configuration option points to an external device. Normally this would be a reasonable assumption, since that’s what the LUKSKEY option is designed to accomplish, but we don’t need or want that; we need init to recognize that the keyfile is already in the filesystem. To do this, we need to fix init’s logic.

The Slackware installer (as of 15.0) should’ve already gone through the trouble of populating /boot/initrd-tree/ for us. A couple more commands to run in there:

cp /boot/initrd-tree/init /boot/init.vanilla
echo '#!/bin/sh' >> /boot/patch-initrd.sh
echo "cp /boot/init.patched /boot/initrd-tree/init" >> /boot/patch-initrd.sh
echo "cp /boot/cryptkey.bin /boot/initrd-tree/cryptkey.bin" >> /boot/patch-initrd.sh
echo "chmod 600 /boot/initrd-tree/cryptkey.bin" >> /boot/patch-initrd.sh
chmod +x /boot/patch-init.sh

We now need to actually create /boot/init.patched. First, we’ll need to copy the original (cp /boot/init.vanilla /boot/init.patched), and then we’ll need to fire up our trusty editor (e.g. nano /boot/init.patched) and hop on down to around line 199, where we’ll see something like this:

    # Determine if we have to use a LUKS keyfile:
    if [ ! -z "$LUKSKEY" ]; then
      mkdir  /mountkey
      KEYPART=$(echo $LUKSKEY |cut -f1 -d:)
      KEYNAME=$(echo $KEYPART |cut -f2 -d=)
      LUKSPATH="/mountkey$(echo $LUKSKEY |cut -f2 -d:)"
      # Catch possible mount failure:
      if blkid |grep "TYPE=\"vfat\"" |grep $KEYNAME 1>/dev/null 2>&1 ; then
        MOUNTOPTS="-t vfat -o shortname=mixed"
      else
        MOUNTOPTS="-t auto"
      fi
      mount $MOUNTOPTS $(findfs $KEYPART) /mountkey 2>/dev/null
      # Check if we can actually use this file:
      if [ ! -f $LUKSPATH ]; then
        LUKSKEY=""
      else
        echo ">>> Using LUKS key file: '$LUKSKEY'"
        LUKSKEY="-d $LUKSPATH"
      fi
    fi

Now, if you’re fluent in Unix shell scripting, you’d probably notice right away that those cut invocations are a bit loosey-goosey. The expectation here (and in man mkinitrd) is that $LUKSKEY is in the format LABEL=${KEYNAME}:${SOME_PATH} (using the manpage’s example: LABEL=TRAVELSTICK:/keys/alien.luks). All fine and dandy, except that cut doesn’t really care if the specified delimiter actually exists; if $LUKSKEY is actually, say, /cryptkey.bin, then let’s see what happens if we do those cut invocations on it:

LUKSKEY=/cryptkey.bin
KEYPART=$(echo $LUKSKEY |cut -f1 -d:)
KEYNAME=$(echo $KEYPART |cut -f2 -d=)
echo $KEYPART
echo $KEYNAME
echo $LUKSKEY |cut -f2 -d:

Your eyes do not deceive you: it’s /cryptkey.bin, every time. Neither Slackware’s mkinitrd script nor its init script does any validation whatsoever of that expected format, so when we pass in that path without any mention of a disk label, init goes kinda bonkers, in the sense that it’ll blindly run mount -t auto /cryptkey.bin /mountkey, see if it worked by looking in /mountkey/cryptkey.bin, and - surprise! - not find anything and thus end up resorting to a passphrase prompt.

Given this, it’s pretty safe to assume that init in its current state always expects a LUKSKEY to include a label and path… meaning that we wouldn’t be breaking any documented functionality by making “only supply a path, leave off the label” mean “this path already exists in the initrd, so just use it as-is”. It just so happens that this is easy to implement by editing this part of init.patched like so:

    # Determine if we have to use a LUKS keyfile:
    if [ ! -z "$LUKSKEY" ]; then
      KEYPART=$(echo $LUKSKEY |cut -f1 -d:)
      KEYPATH=$(echo $LUKSKEY |cut -f2 -d:)
      KEYNAME=$(echo $KEYPART |cut -f2 -d=)
      if [ "$KEYPART" = "$KEYPATH" ]; then
		# We know we won't be able to mount something without a label,
		# so let's just assume that the key is already in the initrd
		# at the specified path.
		LUKSPATH="$KEYPATH"
      else
		mkdir  /mountkey
		LUKSPATH="/mountkey$(echo $LUKSKEY |cut -f2 -d:)"
		# Catch possible mount failure:
		if blkid |grep "TYPE=\"vfat\"" |grep $KEYNAME 1>/dev/null 2>&1 ; then
		  MOUNTOPTS="-t vfat -o shortname=mixed"
		else
		  MOUNTOPTS="-t auto"
		fi
		mount $MOUNTOPTS $(findfs $KEYPART) /mountkey 2>/dev/null
      fi
      # Check if we can actually use this file:
      if [ ! -f $LUKSPATH ]; then
        LUKSKEY=""
      else
        echo ">>> Using LUKS key file: '$LUKSKEY'"
        LUKSKEY="-d $LUKSPATH"
      fi
    fi

Pretty simple logic: if we don’t supply a device label, then $KEYPART and $KEYPATH will equal one another. Save it, run that /boot/patch-initrd.sh, and we’re all set.

Put it all together

With all that taken care of, it’s time to actually build our initrd.gz:

DATA_PARTITION=nvme0n1p2
KERNEL=5.15.19  # as of 15.0 on 2022-02-10; update as needed
cat <<EOF >>/etc/mkinitrd.conf
MODULE_LIST="ext4:hid:drm:usbhid"
LUKSDEV="/dev/${DATA_PARTITION}"
ROOTDEV="/dev/cryptvg/root"
RESUMEDEV="/dev/cryptvg/swap"
LVM=1
EOF
mkinitrd -F -k $KERNEL

We’re now finally ready to reboot.

After the first reboot

From here on out, you’ll have some extra steps to run on every kernel upgrade:

grub-mkconfig -o /boot/grub/grub.cfg
mkinitrd -F -k $(ls /boot/vmlinuz-generic-* | tail -n1 | cut -f3 -d-)

I usually stick that in a script (say, /boot/RUN-AFTER-UPGRADE.sh) so that I only have to remember one post-upgrade step instead of two, but you do you.

Also, if for some reason your /boot/initrd-tree/ gets nuked (say, because you were for some reason compelled to pass the -c option to mkinitrd), you’ll need to re-run that /boot/patch-initrd.sh.

In any case, you should now see that GRUB will immediately prompt for your passphrase on boot, and (if you patched your initrd.gz) you shouldn’t be prompted a second time. At this point you can take measures to lock down that GRUB executable (be it by using Secure Boot, moving it to external media, generating a Coreboot payload, etc.), lock down your firmware, and be reasonably confident that just about anyone trying to tamper with your machine would have a hard time of doing so undetected.