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:
-
Create a giant partition on your chosen disk to hold all the data (we’ll call it
$PARTITION
; usually this would be something likesda1
ornvme0n1p2
or somesuch) -
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) -
Run
cryptsetup luksFormat /dev/$PARTITION
andcryptsetup luksOpen /dev/$PARTITION luks$PARTITION
to turn the partition into an empty encrypted virtual disk and open it for reading/writing -
Run
pvcreate /dev/mapper/luks$PARTITION
andvgcreate $LVM_VG /dev/mapper/luks$PARTITION
(where$LVM_VG
is some name; openSUSE usessystem
, Slackware suggestscryptvg
, but it can be whatever the user wants) to create an LVM physical volume and volume group -
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 (withmkfs.*
ormkswap
) as appropriate -
???
-
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:
-
It’s an extra partition to worry about, and takes up a little bit of space (pretty tiny in this day and age, but still); let’s just call it bad feng shui
-
More importantly, it leaves your kernel, initial ramdisk, GRUB config files, etc. wide open to tampering; you can guard against this with Secure Boot and a “locked down” GRUB and kernel, but it’s still a bit sketchy (it’s a lot more things to sign yourself, and/or a lot more third-party signers to trust)
By eliminating the separate /boot
partition:
-
You end up with a much simpler setup; on (U)EFI systems you only need the EFI system partition (holding only your
grubx64.efi
) and (encrypted) data partition, and if you’re using something like Coreboot you can run GRUB as a payload and do away with that EFI system partition entirely, leaving you with a single fully-encrypted disk -
You end up with a much smaller attack surface, since it’s a lot easier to lock down a single GRUB executable, be it via UEFI’s Secure Boot or as a Coreboot payload; you could even store GRUB in write-protected storage or on a USB stick on your keychain and make your system that much more resistant to tampering
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
/boot
less 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:
-
No
/boot
partition; only EFI (/boot/efi
) and a giant one for everything else -
Skip LILO and ELILO when running the installer
-
Configure GRUB to enable cryptodisk support, then install it
-
Create a LUKS keyfile and shove it into
initrd.gz
(by putting it somewhere in/boot/initrd-tree/
before runningmkinitrd
) -
Patch
init
(by editing/boot/initrd-tree/init
before runningmkinitrd
) to find the keyfile and use it
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.