Customising LVM and LUKS Setup

This is one of those things that I spent a long time working out the first time, then forgot, then worked out again, then forgot and just worked out again. So this time round I'm going to blog it in case it's useful to others as well as my future self.

Some terms in case you've stumbled across this blog: LUKS is the Linux Unified Key Setup, basically it's a standard way of managing encrypted storage. Essentially LUKS defines a standard header that contains information about how a volume is encrypted and then just a big payload space that is mounted as a block device (when the volume is decrypted). What you put in that payload space is up to you, it's commonly just an EXT4 or other file system, but can be something more exotic which is what I'm doing here.

LVM is the Linux Logical Volume Manager, it's an abstraction layer across some storage that allows you to create "logical" volumes from your disks rather than using the disks directly. The primary reason for this is to spread storage across multiple disks as a pool. That way you can spread the storage across whatever storage you have available, move it around as you take disks out of service, expand volumes and allocate new volumes all without taking anything offline.

This is designed for people operating VM farms and other situations where you're managing storage as a pool. It turns out to be useful even for managing a single disk in a laptop. Rather than partitioning the disk which is kinda hard to change later, LVM allows you to break up the disk into logical volumes and allocate storage as you need it. Personally I find that this quite freeing. I like to split /home onto it's own partition, I used to spend a long time thinking about how much space to allocate to the root partition and how much to the home partition. With LVM I just allocate 20 GB to each and add more when I need it.

LVM is particularly powerful when pared with LUKS because you can create one big encrypted storage area using LUKS and then divide it between volumes as you see fit using LVM. This keeps things simple as you only have one volume to decrypt. Of course this also means that you can't selectively decrypt some volumes and not others which may or may not matter depending on your threat model.

This is how the "Encrypt the whole disk" option works in the Ubuntu installer, it creates two physical partitions. The first is a small /boot partition (more on that in another post). The second partition is a LUKS container that fills the rest of the disk. The space within the LUKS container is divided using LVM into a SWAP volume and a root volume.

But what if you want a different layout? The Ubuntu installer like most Linux installers does allow you to define a custom partitioning. For some reason this doesn't allow you to create encrypted disks or LVM volumes. On the other hand, if the volumes already exists, custom partitioning allows you to use them.

Which brings me to the meat of this post: How to create a customized LUKS / LVM layout and install Ubuntu on to that.

For this post I'm going to create a single LUKS container that contains 3 logical volumes, swap, root and home. The same basic process can be used to create more esoteric setups which I may talk about in future posts.

The process can be summed up as:

  1. Create the desired volume structure
  2. Use the Ubuntu installer with custom partitioning to install into that structure
  3. Update /etc/crypttab with the details of any LUKS containers that need to be decrypted
  4. Update /etc/fstab with the details of any volumes that must be mounted
  5. Rebuild the init ram disk so that these are decrypted and mounted during boot

Create the desired volume structure

Rather than relying on the installer to partition the disk I create the desired volume layout first. Then I'll just tell the installer what to put on each volume rather than having it partition the disk for me.

Partition The Physical Disk

Not everything is going to end up in the LUKS / LVM setup. I have EFI and boot partitions that are used in the startup process before the disk is decrypted. So I partition the disk using gparted and create these as physical partitions. I then create a single large partition using the rest of the disk which will become my LUKS container.

For my setup the three partitions are:

  • /dev/sda1 - 512 MB EFI partition
  • /dev/sda2 - 700 MB /boot partition
  • /dev/sda3 - rest of disk for the LUKS container

Create the LUKS Container

Next I create the LUKS container using cryptsetup:

sudo cryptsetup luksFormat /dev/sda3

As part of this I have to provide the disk encryption password. This creates an encrypted volume, but before I can use it I need to "open" it:

sudo cryptsetup open /dev/sda3 main-crypt

main-crypt in the above command is the name given to volume. It can be anything but should be relatively descriptive as it's used to create the device paths. Having opened the LUKS container the block device for it's contents will be at /dev/mapper/main-crypt. The /mapper in this path is because LUKS containers (like many device abstractions) are managed using the Device Mapper. For all intents and purposes this is a regular block device.

Create the LVM Volume Group and Logical Volumes

Next I create an LVM physical volume within the LUKS container:

sudo pvcreate /dev/mapper/main-crypt

In LVM, physical volumes are what provide the actual storage. These are divided into "extents" which are allocated to create the logical volumes used for storage. Before use a physical volume needs to be added to a "volume group". Volume groups are the pools of storage that logical volumes are allocated from. This allows a logical volume to be spread across multiple physical volumes, allows physical volumes to be replaced without interrupting the use of the logical volume and other such operations. For now though I want a volume group that consists of this single volume.

Volume groups are identified by a name, that name (and the other details of the volume group) are written to any physical volumes that make up the volume group. This is really useful because it means the volume group can be activated on a new machine without having to recreate any definition files. Just make the physical volumes available to a machine that has LVM modules (any modern Linux distro) and it will discover and activate the volume group.

What this does mean though is that it's good practice to pick a unique name for each volume group as that makes it easier to recover if the machine fails the disks are mounted on a new machine.

I use the convention machinename-vg, so for this post I'll use mymachine-vg. I create the volume group using vgcreate:

sudo vgcreate mymachine-vg /dev/mapper/main-crypt

One or more physical volumes must be provided to the command so the volume group has some storage. Physical volumes can be added or removed later using vgextend or vgreduce.

After creating the volume group I create the logical volumes using lvcreate:

sudo lvcreate -n swap -L 16G mymachine-vg
sudo lvcreate -n root -L 20G mymachine-vg
sudo lvcreate -n home -L 20G mymachine-vg

The option provided with -n is the logical name of the volume. This is qualified by the volume group name and is used to name the devices. So having run these three commands I have three new block devices created by the device mapper:

  • /dev/mapper/mymachine--vg-swap
  • /dev/mapper/mymachine--vg-root
  • /dev/mapper/mymachine--vg-home

For convenience, recent versions of LVM also create a folder with the volume group name, this allows the volumes to be referenced without escaping the names. So these same three devices are aliased at:

  • /dev/mymachine-vg/swap
  • /dev/mymachine-vg/root
  • /dev/mymachine-vg/home

And that's it for setup, I now have all the volumes I need:

  • EFI volume /dev/sda1
  • /boot volume /dev/sda2
  • swap volume /dev/mymachine-vg/swap
  • root volume /dev/mymachine-vg/root
  • /home volume /dev/mymachine-vg/home

Use the Ubuntu installer with custom partitioning to install into that structure

Since the volumes now exist, I run the Ubuntu installer and select the custom partitioning mode. The installer detects the various partitions and device mapper devices, so rather than repartitioning the disk I simply indicate what part of the system should reside on each device. In this case:

  • /dev/sda1 - Use As EFI
  • /dev/sda2 - Format EXT4, mount at /boot
  • /dev/mymachine-vg/swap - Use as swap
  • /dev/mymachine-vg/root - Format as EXT4, mount at /
  • /dev/mymachine-vg/home - Format as EXT4, mount at /home

One gotcha is that the LVM volume group must be active, so if I've rebooted after creating the volumes I have to remember to open the LUKS container using cryptsetup open as above.

Running the installer with these volumes selected will install all the relevant parts of the system and will create the appropriate /etc/fstab file to mount these volumes where they should be.

However it doesn't know that they reside on an encrypted disk, so before booting into my new Linux install I need to tell Ubuntu about the LUKS partition.

Update /etc/crypttab with the details of any LUKS containers that need to be decrypted

The /etc/crypttab file tells a Linux install how to deal with encrypted partitions. This is the file I need to create within the root partition so that Ubuntu will know to decrypt the disk during boot.

To do this I need to first mount the root partition so I can create a file in it:

sudo mount /dev/mymachine-vg/root /mnt

/etc/crypttab contains one line per encrypted file system, the fields within the line are separated by spaces or tabs. The fields are:

  • Name of the container - This is the same concept as when I opened the container with cryptsetup open
  • Device to open - this can be a device path like /dev/sda3 but it's much better to use the device UUID as that way it doesn't matter if the device gets mapped to a different path
  • The path to a keyfile to be used as the password to decrypt the device - for this setup I'm using a password so set this to none
  • A comma separated list of options - There are lots of options listed in the crypttab man page, there are only two that are really relevant for this case, luks and discard. The first just says that it's a LUKS container (there are other encryption options, but LUKS is the simplest). I'll talk about discard a bit below, but in general I set it because it's the default.

The only bit of information I don't have yet is the device UUID, I can get that by running blkid as root. That lists the UUID of each volume (amongst other info).

So armed with that information I create /mnt/etc/crypttab as shown below:

main-crypt UUID=887869e4-2888-11ea-978f-2e728ce88125 none luks,discard

One thing that consistently trips me up is the fact that blkid returns the UUIDs in quotes but it must not be quoted in crypttab.

The discard option allows the use of the TRIM instruction with the underlying device. Essentially this means that the encrypted volume can tell the underlying drive what space is unused. This allows SSDs to be a little bit more efficient since they know what parts of the disk are unused. But it does leak slightly more information since an attacker can know what parts of the disk are used. This is a bit beyond me but there's a good write up of how TRIM interacts with encryption

I just tend to set discard because of this entry in the crypttab man page:

Starting with Debian 10 (Buster), this option is added per default to new dm-crypt devices by the Debian Installer. If you don't care about leaking access patterns (filesystem type, used space) and don't have hidden truecrypt volumes inside this volume, then it should be safe to enable this option. See the following warning for further information.

Update /etc/fstab with the details of any volumes that must be mounted

/etc/fstab contains all the volume mounts. It's worth checking this is correct, usually the installer creates it correctly based off the mount points specified during the install process.

What I have found is that, for LVM volumes, it tends to create the mounts with the device path rather than the UUID. This is less of a problem than it appears since device mapper paths are pretty stable, but I prefer to use the UUIDs just in case. So I tend to replace these with UUID mappings as in crypttab.

Rebuild the init ram disk so that these are decrypted and mounted during boot

During bootup, the system will need the information from /etc/crypttab and /etc/fstab to decrypt and mount the volumes. But these files are not available till after the volume is decrypted and mounted (since they're within the encrypted disk). Linux solves these and other bootstrapping problems through the init ram disk. Essentially there is an archive of a file system that's unpacked into a ram disk during boot. This contains the drivers and instructions for how to mount the various volumes needed to actually boot the system. Crazy as it sounds, the modern Linux boot process basically involves standing up a minimal Linux distro from an easily accessible disk, in order to run the scripts which mount the less easily accessible disks.

The init ram disk archives lives in the /boot directory. These are the various /boot/initrd.img-* files (one per kernel version). This is why I keep the /boot partition separate and unencrypted as it makes this bootstrapping simple. There are other options but I've found having /boot unencrypted is the simplest.

So to make my new Linux install bootable I need to update the init ram disk image with the instructions to decrypt and mount the volumes I created.

On Ubuntu and other Debian based distros, the init ram disk image is maintained using initramfs-tools. This is a set of scripts that work out what's needed during boot by inspecting /etc/crypttab. /etc/fstab and other files. So what I need to do is chroot to the root volume and run update-initramfs.

To make the chroot complete I need to bind mount a bunch of special directories into the root volume. The directories I need to bind mount are /proc, /sys, /dev, /dev/pts and /run. So I bind mount these all:

for dir in /proc /sys /dev /dev/pts /run; do sudo mount --bind $dir /mnt/$dir; done

Then I chroot into the root volume, effectively this changes the root of the file system so it looks as if the root volume is actually at the root as it would be when I boot into the Linux install.

sudo chroot /mnt bash

Then I mount the remaining volumes based on /etc/fstab:

mount -a

Now that I'm effectively working in the root of my install I can run update-initramfs to generate the new image for the init ram disk:

update-initramfs -u

And that's it, reboot the machine and it decrypts and mounts the disks during the boot process.

Summary

That seemed like rather more steps than I remembered, but at lest in concept the process is pretty simple.

# 1.  Boot into the LiveUSB

# 2.  Partition the physical disk using `gparted` or similar

# 3.  Create the encrypted LUKS container and open it
sudo cryptsetup luksFormat <DATA_PARTITION_DEVICE>
sudo cryptsetup open <DATA_PARTITION_DEVICE> main-crypt

# 4.  Create the LVM Volume Group
sudo pvcreate /dev/mapper/main-crypt
sudo vgcreate <VOLUME_GROUP_NAME> /dev/mapper/main-crypt

# 5.  Create the logical volumes
sudo lvcreate -n swap -L 16G <VOLUME_GROUP_NAME>
sudo lvcreate -n root -L 20G <VOLUME_GROUP_NAME>
sudo lvcreate -n home -L 20G <VOLUME_GROUP_NAME>

# 6.  Run the installer and select the logical volumes rather than repartitioning the disk

# 7.  Mount the root volume
sudo mount /dev/<VOLUME_GROUP_NAME>/root /mnt

# 8.  Create the `/etc/crypttab` file within the mounted root volume with the details of the LUKS container to decrypt

# 9.  Update the `/etc/fstab` file within the mounted root volume to ensure the right volumes are mounted in the right places

# 10. Establish chroot into the root volume
for dir in /proc /sys /dev /dev/pts /run; do sudo mount --bind $dir /mnt/$dir; done
sudo chroot /mnt bash

# 11. Update the init ram disk image with the updated crypttab and fstab
update-initramfs -u

# 12. Reboot and enjoy

Comments !

social