LUKS Disk Encryption With New Gentoo Linux Install

September 27, 2023 —

Over the past month, I've re-built a couple different Gentoo Linux systems of mine with LUKS disk encryption. The first time, I stumbled through it and made many mistakes. The next time, I made some of the same mistakes again. Oops. In response to that, I figured I'd help myself out by writing a post that I can refer back to on the process.

Assumptions

As already mentioned above, this will be written from the perspective of a new install of Gentoo Linux.

I'll be using:

  • Passphrase-based disk encryption with cryptsetup and LUKS
  • A very simple disk partition scheme. No LVM / separate partitions for the system files.
    Just /boot and / partitions.
  • A distribution kernel
  • Dracut to build the initramfs which is the default for distribution kernel's anyway.
  • systemd
  • rEFInd

It should be noted that the use of rEFInd does prevent us from also encrypting our /boot files which is where our kernel images will be stored. I don't believe there's a way to get rEFInd to boot such a system, but I would love to be proven wrong, so if you're reading this and you know of a way, please let me know! In the mean time, if this is important to you, check out this extremely detailed guide covering an alternative method of setting up LUKS disk encryption (that includes /boot too) but with Grub instead.

Disk Preparation

Optional Step for NVMe Drives

Before we even begin, I'm going to assume that in 2023 and beyond, that you're probably using an SSD, probably even an NVMe SSD. If so, then this may be a great time to check on your NVMe drive's sector size and whether changing it might be beneficial to you for performance reasons.

Install the sys-apps/nvme-cli package so you can use the nvme tool.

# emerge -av sys-apps/nvme-cli

Then we can check what our NVMe drive's current LBA format is, and what others may be available:

# nvme id-ns -H /dev/nvme0n1 | grep "Relative Performance"
LBA Format  0 : Metadata Size: 0   bytes - Data Size: 512 bytes - Relative Performance: 0x2 Good (in use)
LBA Format  1 : Metadata Size: 0   bytes - Data Size: 4096 bytes - Relative Performance: 0x1 Better

In this case, my NVMe drive is a WD Black SN850X 2TB. We can see there's another 4KB size LBA format that offers better performance.

To give you an idea of the performance difference that is possible, here's a simple hdparm read test on this same NVMe drive with the default LBA format with 512 byte sector size, performed on a LUKs encrypted partition on that same drive (so, measuring the decryption and read speed together):

# hdparm -tT /dev/mapper/myCryptRootfs

/dev/mapper/myCryptRootfs:
 Timing cached reads:   26614 MB in  2.00 seconds = 13330.47 MB/sec
 Timing buffered disk reads: 3802 MB in  3.00 seconds = 1267.30 MB/sec

Here's the same test performed again on the same NVMe drive, but after re-formatting the drive with the other LBA format with a 4KB sector size:

# hdparm -tT /dev/mapper/myCryptRootfs

/dev/mapper/myCryptRootfs:
 Timing cached reads:   26232 MB in  2.00 seconds = 13138.91 MB/sec
 Timing buffered disk reads: 6002 MB in  3.00 seconds = 2000.40 MB/sec

Not a bad boost, honestly!

If anyone is reading this who actually owns this very same NVMe drive and is thinking that the speeds shown above are maybe a bit slow even after accounting for the fact that the data is being decrypted ... well, you're right, it is slower than it should be in my system. It's slower because I ran out of M.2 connections on my motherboard and had to buy a PCIe adapter, which apparently isn't running the drive at full speed. Oh well.

To switch the drive's LBA format, we unfortunately need to completely re-format the drive!

# nvme format --lbaf=1 /dev/nvme0n1
You are about to format nvme0n1, namespace 0x1.
WARNING: Format may irrevocably delete this device's data.
You have 10 seconds to press Ctrl-C to cancel this operation.

Use the force [--force] option to suppress this warning.
Sending format operation ...
Success formatting namespace:1

The --lbaf=1 parameter specifies the number of the LBA format we saw in the previous output from nvme. That is, the "LBA Format 0" and "LBA Format 1" lines. After running the nvme format command like in the above example, you can re-run the previous command to check that the LBA format you specified was set.

Some drives don't have alternate LBA formats that can be set via the nvme tool. For example, I also have an OEM Samsung 980 Pro NVMe drive, and I only see a single LBA format available:

# nvme id-ns -H /dev/nvme2n1 | grep "Relative Performance"
LBA Format  0 : Metadata Size: 0   bytes - Data Size: 512 bytes - Relative Performance: 0 Best (in use)

I've heard that NVMe drives from certain manufacturers may require the use of their own special tools to change these settings. But I've never checked into this for myself, so I cannot say for certain.

Partitioning

There's nothing special here. I'm going with the following scheme:

  • 512MB /boot which will contain the EFI stuff too at /boot/EFI.
  • The remainder of the disk will be a single partition. For my own personal desktops/laptops these days I don't ever feel the need to bother with LVM, or even just plain separate partitions for things like /home, /var, /opt, etc.

So in the end, using your disk partition tool of choice, you'd end up with something that looks like:

# fdisk -l /dev/nvme0n1
Disk /dev/nvme0n1: 1.82 TiB, 2000398934016 bytes, 488378646 sectors
Disk model: WD_BLACK SN850X HS 2000GB
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: gpt
Disk identifier: 8183DFBE-8C57-0744-80E2-C0B958764776

Device          Start       End   Sectors  Size Type
/dev/nvme0n1p1    256    131327    131072  512M EFI System
/dev/nvme0n1p2 131328 488378623 488247296  1.8T Linux filesystem

Cryptsetup and LUKS Volume Creation

We need sys-fs/cryptsetup so we can use the cryptsetup tool to manage our LUKS volumes.

# emerge -av sys-fs/cryptsetup

Of course, if you're preparing this new Gentoo install at this point from another Linux system that isn't Gentoo, then install cryptsetup as per whatever package manager you have available.

Also, we'll need to make sure that the "dm-crypt" kernel module is loaded.

# modprobe dm-crypt
$ lsmod | grep dm_crypt
dm_crypt               57344  0

Ok, now we're ready to set up our LUKS encrypted volume.

If you run cryptsetup --help first you can see what some default settings are, shown at the bottom of the help output:

Default compiled-in metadata format is LUKS2 (for luksFormat action).

LUKS2 external token plugin support is compiled-in.
LUKS2 external token plugin path: /usr/lib64/cryptsetup.

Default compiled-in key and passphrase parameters:
        Maximum keyfile size: 8192kB, Maximum interactive passphrase length 512 (characters)
Default PBKDF for LUKS1: pbkdf2, iteration time: 2000 (ms)
Default PBKDF for LUKS2: argon2id
        Iteration time: 2000, Memory required: 1048576kB, Parallel threads: 4

Default compiled-in device cipher parameters:
        loop-AES: aes, Key 256 bits
        plain: aes-cbc-essiv:sha256, Key: 256 bits, Password hashing: ripemd160
        LUKS: aes-xts-plain64, Key: 256 bits, LUKS header hashing: sha256, RNG: /dev/random
        LUKS: Default keysize with XTS mode (two internal keys) will be doubled.

So, LUKS2 is the default, aes-xts-plain64 is the default cipher, and the key size is 256 bits. It doesn't actually say in this snippet of the output I've shared here (if you scroll up a bunch in the help text you can find it in the description of the --sector-size parameter), but the default encryption sector size is 512 bytes. We'll want to adjust that to match our 4KB sector size of the NVMe drive we're using for best performance.

Now then, lets create the LUKS volume on our / partition.

# cryptsetup luksFormat --sector-size=4096 --key-size=512 /dev/nvme0n1p2

Note we explicitly set the sector size to match our NVMe drive's LBA format. And we bump up the key size to 512 for a bit of extra security.

You'll be asked to enter a passphrase here. Make sure it's secure! That's the whole point of this after all ...

Now we can open and map the LUKS volume so that we can access the volume as decrypted data.

# cryptsetup luksOpen /dev/nvme0n1p2 myCryptRootfs

The "myCryptRootfs" bit at the end is the name for the mapped device that will appear under /dev/mapper. You can use any name you like here. But for the rest of this post I will be using this name.

Here we can verify that our LUKS volume was created to our desired specifications:

# cryptsetup status myCryptRootfs
/dev/mapper/myCryptRootfs is active.
  type:    LUKS2
  cipher:  aes-xts-plain64
  keysize: 512 bits
  key location: keyring
  device:  /dev/nvme0n1p2
  sector size:  4096
  offset:  32768 sectors
  size:    3905945600 sectors
  mode:    read/write

As you can see here, the device is available at /dev/mapper/myCryptRootfs. At this point, you can proceed with the rest of your Gentoo install as per the handbook. When you need to create a filesystem on the rootfs partition and then mount it, just use /dev/mapper/myCryptRootfs for the device. For example:

# mkfs.ext4 -L my_rootfs /dev/mapper/myCryptRootfs
# mount /dev/mapper/myCryptRootfs /mnt/gentoo

When you're done with a LUKS volume, you need to unmount the mapped device and then close the LUKS volume.

# umount /dev/mapper/myCryptRootfs
# cryptsetup luksClose myCryptRootfs

System Configuration

Systemd

In the actual Gentoo system that you're installing (and by now are probably chroot'ed into it if you're following the handbook) you'll want to add USE="cryptsetup" to your /etc/portage/make.conf (or I guess you could just add it for sys-apps/systemd, but I've just been adding it globally every time I've done this).

Then you'll want to rebuild systemd with this new cryptsetup use flag. This ensures that the necessary cryptsetup bits are copied into the kernel's initramfs.

# emerge -avuDN world

Dracut

Once you've installed your distribution kernel (e.g. sys-kernel/gentoo-kernel), we'll need to update our Dracut configuration:

# mkdir /etc/dracut.conf.d
# echo 'add_dracutmodules+=" crypt dm rootfs-block "' > /etc/dracut.conf.d/luks.conf

Then we need to rebuild our kernel's initramfs using a command similar to the below (substitute the package with your own choice for distribution kernel that you used):

# emerge -av --config sys-kernel/gentoo-kernel

fstab

In your /etc/fstab, be sure to specify attributes (such as LABEL or UUID) for the /dev/mapper/myCryptRootfs device, not the encrypted partition itself.

rEFInd

One of the trickest parts for me was getting the kernel command line arguments correct. Since we're using Dracut, the arguments we must use are different than if we were not when it comes to specifying LUKS volume UUIDs.

  • rd.luks.uuid should specify the UUID of the raw encrypted disk partition.
  • root should specify the UUID of the opened / decrypted LUKS mapped device.

So, to put this all together. In my system if I check the blkid output:

# blkid
/dev/nvme0n1p1: LABEL_FATBOOT="BOOT" LABEL="BOOT" UUID="D792-8C94" BLOCK_SIZE="4096" TYPE="vfat" PARTUUID="31f9a04a-7803-1540-929e-4ec2bd4c33d6"
/dev/nvme0n1p2: UUID="bbcea224-b849-47b2-a958-9d27007dfe33" TYPE="crypto_LUKS" PARTUUID="321e5973-13ca-f543-a5a1-d156efc8b576"
/dev/mapper/myCryptRootfs: LABEL="ROOTFS" UUID="bffff17b-7948-43cf-9b35-f9b1b745db0d" BLOCK_SIZE="4096" TYPE="ext4"

So, my kernel command line arguments should have something like:

ro rd.luks.uuid=bbcea224-b849-47b2-a958-9d27007dfe33 root=UUID=bffff17b-7948-43cf-9b35-f9b1b745db0d

Where bbcea224-b849-47b2-a958-9d27007dfe33 refers to the raw encrypted disk partition /dev/nvme0n1p2 and bffff17b-7948-43cf-9b35-f9b1b745db0d refers to the opened / decrypted LUKS mapped device /dev/mapper/myCryptRootfs.

If you're using a swapfile that exists directly on the encrypted partition and you're also intending on using it to hibernate your system with, you'll need to also point the resume kernel command line argument to the opened / decrypted LUKS mapped device (/dev/mapper/myCryptRootfs in our example case here).

So, a more complete set of kernel command line arguments that includes the necessary bits for system hibernation:

ro rd.luks.uuid=bbcea224-b849-47b2-a958-9d27007dfe33 root=UUID=bffff17b-7948-43cf-9b35-f9b1b745db0d resume=UUID=bffff17b-7948-43cf-9b35-f9b1b745db0d resume_offset=268404736

Where resume_offset is obviously replaced with the value you get yourself via usage of the swap-offset utility as the above linked article on hibernation explains.

Now you can place your final set of kernel command line arguments in your /boot/refind_linux.conf file and finalize your new Gentoo install.

The End

That's all there is to it. Quite a bit actually once I see it all written out like this. Maybe one day the process will get refined to where it's as simple as clicking a button à la Mac OS's FileVault. One day ...