Using cryptsetup’s native Opal support to decrypt self-encrypting drive partitions at boot with LUKS and systemd

Prior to cryptsetup 2.7.0, using self-encrypting drives (SEDs) on Linux required the use of tools like sedutil to boot in order to use hardware encryption, otherwise the drives were limited to using LUKS software encryption.

If you want to encrypt your root or boot partitions via the Opal specification for hardware encryption, you either needed a motherboard with UEFI/BIOS that supports Opal, or you need to flash a pre-boot authorization (PBA) image to your Opal drive and boot from that.

On systems without Opal support, SEDs will trick firmware into booting an unencrypted PBA image that then handles the mounting and booting of the encrypted disk itself.

With the new release of cryptsetup, you can use self-encrypting drives’ hardware encryption directly with LUKS via Opal interfaces.

Click here to skip directly to the tutorial 👇

Hardware encryption versus software encryption

This comes down to choice. Modern NVMe drives with self-encryption support automatically encrypt your data even if you don’t set up encryption yourself. The drives ship with a manufacturer-set key that encrypts your data, but with the password disabled. Even if you personally aren’t using a password to unlock your drive, your data is still being encrypted anyway.

By shipping drives with encryption turned on without a password, it allows you to later set a password for your drive without having to erase all of your data just so that it can be encrypted using your password. Explicitly setting a password just encrypts the drive’s underlying encryption key, and requires that password to decrypt the drive.

tl;dr: Your self-encrypting drives are already using encryption even if it is “off”. You’re already paying for the cost of hardware self-encryption whether you set a password or not.

Software encryption, on the other hand, needs to be explicitly set up. Using software encryption has a cost: it takes time and energy to encrypt and decrypt data. All of your data must flow through your CPU in order to read or write anything to disk, which takes time and a considerable amount of power, even if your processors implement AES-NI acceleration.

If you’re on a laptop, for example, software encryption can impact your battery life. It can also impact the speed at which you’re able to access data on your machine. Comparing the two, you don’t have to pay any additional cost in terms of speed or battery life if you choose to enable hardware encryption, since it’s always on anyway.

That said, by using hardware encryption alone, you are placing full trust in your hardware manufacturer’s encryption implementation. That trust can be misplaced, as self-encrypted drives’ firmware implementations are frequently broken. Even if you’re using strong underlying encryption, if the surrounding implementation can be hacked, that encryption doesn’t mean much if an attacker can easily exfiltrate your keys or data.

When it comes to encryption, mobile users are presented with the choice to use software encryption, and pay the price in terms of time and energy, or use hardware encryption, which can improve speed and battery life at the expense of potentially using poor encryption implementations.

If your machine is stolen, hardware encryption might keep your data safe from unsophisticated criminals, but it might not keep you out of prison if you’re a whistleblower that a government wants to silence. For 99.999% of people, keeping data safe from petty criminals is sufficient enough. Just keep in mind that it’s not perfect and don’t hinge your life or freedom on some random drive’s encryption implementation.

Booting self-encrypted drives on machines without Opal support

My hardware doesn’t support self-encrypting drives natively, so if I want to encrypt my root and/or boot partitions, I need to jump through some hoops. Those hoops include finding or building PBA images, enabling pre-boot authorization on my self-encrypting drives, ensuring my PBA is set up to boot correctly and configuring the system to use my authentication methods of choice.

Luckily, it turns out that the Drive Trust Alliance (DTA) published pre-compiled PBA images for use with sedutil. That sounds convenient, until you learn that those images were compiled 4+ years ago using kernels that may or may not support your hardware. Similarly, if you want to unlock your Opal drives using multiple factors, like FIDO authenticators, you need to modify your PBA image to do so. The PBA from the DTA doesn’t support hardware authenticators like Yubikeys natively.

On normal drives that do not have encryption support, you can use software encryption via cryptsetup, LUKS and systemd. It’s easy to set up and boot your encrypted drives, and cryptsetup natively supports enrolling FIDO authenticators to unlock encrypted partitions. Set it up right, and you can easily use a Yubikey at boot to unlock your root partitions. It’s relatively uncomplicated.

In contrast, using self-encrypting drives is much more cumbersome, and potentially involves rolling your own bootable PBA image just to unlock your drive.

That was the state of things if you wanted to use Opal with motherboard firmware that doesn’t support self-encrypting drives.

New cryptsetup has native Opal support

Now, with cryptsetup 2.7.0, there is native support for hardware encryption for drives that support Opal encryption, even if your firmware doesn’t implement support for Opal drives.

Set up correctly, you can use cryptsetup, LUKS and systemd with hardware encryption as easily as it is to use software encryption. There is no need to trick your BIOS into booting a PBA, and there is no need to build your own PBA image if you want to use a Yubikey. In fact, there is no need for a PBA at all.

Typical encrypted file-system scheme with cryptsetup

When using software encryption, it’s typical to turn on Secure Boot and leave your /boot unencrypted so you can bootstrap your kernel and initramfs to automatically unlock your encrypted / for you. This can be automated so that all you need to do is supply your password, and/or Yubikey + PIN, at boot to unlock your root partition.

Depending on how much of the Opal specification your self-encrypting drive accurately implements, Opal drives can use hardware encryption for specific regions of the drive while leaving others unencrypted.

That means you can use a similar setup to your software encryption scheme where you turn on Secure Boot and leave your /boot unencrypted so you can easily unlock your /. All you have to do is create a /boot EFI partition and a separate partition for /. You can then tell your Opal drive to encrypt your / region.

From that point on, you can use hardware encryption with the same exact tools you would software encryption, allowing you to easily boot, enter your credentials and unlock your drive without a hacky pre-boot authorization process complicating your boot process.

How to use hardware encryption with cryptsetup

This guide assumes you’re using systemd-boot and systemd-cryptsetup-generator to unlock your LUKS partitions at boot. I add additional information and links for those who are not using this setup, but be aware that this guide has only been tested using systemd for boot and cryptsetup support.

cryptsetup only works with self-encrypting drives over PCIe, not USB.

Find your NVMe device in /dev. Mine is /dev/nvme1n1. Yours might be different.

Detect Opal support

Not all drives support the Opal specification. You can see if your drive supports it by running the following:

$ sudo sedutil-cli --scan

Scanning for Opal compliant disks
/dev/nvme1  2  YOUR_DRIVE_MODEL		YOUR_DRIVE_ID
No more disks present ending scan

If you see a number in the second column, like the 2 above, your drive supports Opal.

If you see the word No, your drive does not support Opal:

$ sudo sedutil-cli --scan

Scanning for Opal compliant disks
/dev/nvme0 No  YOUR_DRIVE_MODEL		YOUR_DRIVE_ID
No more disks present ending scan

Reset your Opal drive

This will erase your drive and reset it.

You will need to locate the PSID of your drive to prove you have physical access to it. On my drives, it is printed in small text on a sticker attached to the drives themselves. If the PSID has dashes, ignore them. The PSID should be one long string of letters and numbers without any special symbols.

When you find it, save it somewhere you won’t lose it and then use sedutil-cli with your drive:

$ sudo sedutil-cli --PSIDrevert "YOUR_PSID_HERE" /dev/nvme1n1

Or you can use cryptsetup instead:

$ cryptsetup luksErase --hw-opal-factory-reset /dev/nvme1n1
Enter OPAL PSID: YOUR_PSID_HERE

Replace YOUR_PSID_HERE with your drive’s PSID.

Alternatively, if you’ve already set up encryption on your drive’s specific locking range, and know the passwords you set yourself, you can skip the stage above. Keep in mind that setting up LUKS on an Opal locking range will reset and erase that locking range, anyway.

Create boot and root partitions

This will overwrite your partition table and you will lose the data on the drive.

Create a /boot EFI partition and a root partition using parted.

$ sudo parted /dev/nvme1n1

Inside parted, run:

# Create a GPT partition table
mklabel gpt

# Create an EFI partition
mkpart ESP fat32 1MiB 513MiB
set 1 boot on

# Create a root partition that we'll encrypt
mkpart primary btrfs 513MiB 100%

quit

Format your EFI partition:

$ sudo mkfs.fat -F32 /dev/nvme1n1p1

Set up LUKS with Opal support

Running luksFormat will erase and format your specified partition, you will lose the data on it. Encrypting data in-place is not supported.

Ensure that you have cryptsetup 2.70 or newer. Older versions of cryptsetup will not work.

Determine your root partition’s location in /dev. My EFI partition is /dev/nvme1n1p1 and my root partition to be encrypted is /dev/nvme1n1p2.

Then run the following to set up LUKS:

$ sudo cryptsetup luksFormat /dev/nvme1n1p2 --type luks2 --hw-opal-only

The --hw-opal-only flag tells cryptsetup to use hardware encryption only. If you want to use software encryption on top of hardware encryption, pass the --hw-opal flag instead.

You might be asked to set user and administrator password, do so and don’t forget them.

Let’s open and decrypt the partition so we can format a file-system on it. You can replace DISK_NAME_GOES_HERE with any name, the value will determine what Device Mapper will call your unlocked drive under /dev/mapper.

$ sudo cryptsetup open /dev/nvme1n1p2 DISK_NAME_GOES_HERE
$ sudo mkfs.btrfs -L EncryptedRoot /dev/mapper/DISK_NAME_GOES_HERE

I’m using btrfs, but feel free to use EXT4, F2FS or whatever you want for your root partition. Note that the rest of this guide will assume you’re using btrfs, too.

Enroll your authentication methods

LUKS partitions can be unlocked via a variety of methods, like passwords, key-files and hardware authenticators. Enroll the methods you’d like to use to decrypt your drive at boot.

If you want to use a FIDO authentication device, you can use systemd-cryptenroll to enroll one yourself:

$ systemd-cryptenroll --fido2-device=auto /dev/nvme1n1p2

For plaintext passwords, you can run:

$ systemd-cryptenroll /dev/nvme1n1p2 --password

Here is a good resource for using systemd-cryptenroll to enroll various authentication methods with LUKS.

Confirm configuration

Before going any further, let’s ensure that the encrypted partition is set up properly.

Close your open LUKS partition:

$ sudo cryptsetup close DISK_NAME_GOES_HERE

Then try to open it again:

$ sudo cryptsetup open /dev/nvme1n1p2 DISK_NAME_GOES_HERE --token-only

Omit the --token-only flag if you aren’t unlocking your partition with something like a Yubikey.

You should now see your device in /dev/mapper:

$ ls /dev/mapper
/dev/mapper/DISK_NAME_GOES_HERE

Ensure your LUKS partition is set up to your expectations:

$ sudo cryptsetup luksDump /dev/nvme1n1p2

Set your machine to unlock LUKS

We’re going to need to create a /etc/crypttab file and edit the kernel command-line options to point the kernel to our encrypted root file-system.

Create a crypttab file

Determine your partition’s UUID by using blkid and noting its PARTUUID label:

$ blkid
/dev/nvme1n1p2: UUID="87425985-0ed3-486f-8d66-b07bbf27f8ea" TYPE="crypto_LUKS" PARTLABEL="EncryptedRoot" PARTUUID="fec2fe57-8a1b-43af-96b4-05cf1b944e03"

With root permissions, create a /etc/crypttab file like so, replacing the UUID value with the PARTUUID value of your partition:

# <name>               <device>                         <password> <options>
DISK_NAME_GOES_HERE      UUID=PARTUUID_GOES_HERE     -     luks

A common naming scheme is just prepending your UUID with luks-.

Here’s an example crypttab:

# <name>               <device>                         <password> <options>
luks-fec2fe57-8a1b-43af-96b4-05cf1b944e03      UUID=fec2fe57-8a1b-43af-96b4-05cf1b944e03     -    luks,discard,fido2-device=auto,token-timeout=10

Your options should reflect your unlocking method you used with systemd-cryptenroll. If you used a FIDO authenticator, add fido2-device=auto to your options. Similarly, you can set a token timeout. If you’re using a password, skip the fido2 and token timeout options.

If you want to enable TRIM on the partition, add the discard option to your crypttab. Keep in mind that using TRIM has security implications.

Install your root file-system

You can copy over an existing file-system using something like rsync or cp. If you’re using btrfs, you can use btrfs send and btrfs receive to copy your root subvolume(s) over.

You can also use debootstrap or pacstrap to bootstrap a new Debian or Arch rootfs.

You might want to create a root subvolume:

$ mkdir /tmp/disk
$ sudo mount /dev/mapper/DISK_NAME_GOES_HERE /tmp/disk
$ btrfs subvolume create disk/@
$ sudo umount /tmp/disk

Configure /etc/fstab

You need to set your root file-system entry correctly.

Instead of pointing directly to your disk, you need to point / to the value of the custom name you set in /etc/crypttab.

Edit your /etc/fstab file to point to your unlocked LUKS device in /dev/mapper:

/dev/mapper/DISK_NAME_GOES_HERE      /       btrfs   subvol=/@,defaults 0 0

The above assumes your / has a /@ subvolume used as the root subvolume.

You can also use other btrfs features like noatime, compress=zstd, ssd, discard=async, autodefrag:

/dev/mapper/luks-fec2fe57-8a1b-43af-96b4-05cf1b944e03       /     btrfs     subvol=@,defaults,noatime,compress=zstd,ssd,discard=async,autodefrag 0 0

Alternatively, you can use the unlocked LUKS volume’s UUID instead of pointing to /dev/mapper/DISK_NAME_GOES_HERE.

Make sure you have an entry for your EFI partition in /etc/fstab. You might not be able to boot without it.

Unlock at boot

Edit kernel command-line options

On my system, I can edit my kernel command-line options by changing the /etc/kernel/cmdline file.

We need to append the rd.luks.uuid and root parameters:

rd.luks.uuid=PARTUUID_GOES_HERE root=/dev/mapper/DISK_NAME_GOES_HERE

This is how it would look using the example values from above:

rd.luks.uuid=fec2fe57-8a1b-43af-96b4-05cf1b944e03 root=/dev/mapper/luks-fec2fe57-8a1b-43af-96b4-05cf1b944e03

After adding those parameters, make sure they’re propagated to your bootloader like systemd-boot or GRUB.

On EndeavourOS, for example, simply running sudo reinstall-kernels will ensure that the new kernel command-line parameters will be supplied at boot.

Configure your early boot environment

You might have to configure mkinitcpio or dracut so that you can unlock your drive at boot.

You might have to add the following modules to /etc/mkinitcpio:

  • keyboard
  • systemd
  • sd-vconsole
  • sd-encrypt

With dracut, you might need to add crypttab to a configuration file in /etc/dracut.conf.d:

install_items+=" /etc/crypttab "

You might also have to add the systemd and resume modules to dracut:

add_dracutmodules+=" systemd resume "

Again, do whatever you need to do on your distribution to update your initramfs. For example, reinstall-kernels will do that automatically.

Reboot

Cross your fingers and reboot. If everything went well, your system will boot to a prompt asking for you to authenticate and unlock your root partition.

Encrypted swap and hibernation

Once your encrypted root partition is set up and you’ve booted into it, you might want to set up encrypted swap, too.

Create a swap file

Btrfs gives us some tools that make setting up an encrypted swap with hibernation support easy.

Mount your LUKS partition to create a swap subvolume:

$ mkdir /tmp/root
$ sudo mount -o subvol=/@ /dev/mapper/DISK_NAME_GOES_HERE /tmp/root
$ sudo btrfs create subvolume /tmp/root/swap

You’ll want to put it in its own subvolume because you cannot take snapshots of subvolumes that contain swapfiles.

Choose the size of your swap file, if you want to enable hibernation, it needs to be the same size, or larger, than the amount of memory you have.

$ sudo btrfs filesystem mkswapfile --size 64G --uuid clear /tmp/root/swap/swapfile

Configure fstab

Add your swap file to your /etc/fstab:

/swap/swapfile      swap        swap        defaults    0   0

Reload systemd and test out the swap:

$ sudo systemctl daemon-reload
$ sudo swapon -a

Get the swap file offset

Get the resume offset:

$ btrfs inspect-internal map-swapfile -r /tmp/root/swap/swapfile
198273664  # YOUR_RESUME_OFFSET

Get your LUKS partition UUID

Get your unlocked LUKS disk’s UUID:

$ blkid
/dev/mapper/DISK_NAME_GOES_HERE: UUID="YOUR_LUKS_UUID" UUID_SUB="IGNORE_THIS" BLOCK_SIZE="4096" TYPE="btrfs"

Here’s an example output:

$ blkid
/dev/mapper/luks-fec2fe57-8a1b-43af-96b4-05cf1b944e03: UUID="ae72c069-9729-43dc-adab-ea0054ef70ee" UUID_SUB="511b5768-dd75-453e-9ecf-a194e9167f2b" BLOCK_SIZE="4096" TYPE="btrfs"

In this case, you’d want to keep the UUID value ae72c069-9729-43dc-adab-ea0054ef70ee and ignore the rest.

Adjust your kernel parameters

Edit your kernel’s command line options. On my system, that means editing /etc/kernel/cmdline:

resume=UUID=YOUR_LUKS_UUID resume_offset=YOUR_RESUME_OFFSET

Using the example parameters:

resume=UUID=ae72c069-9729-43dc-adab-ea0054ef70ee resume_offset=198273664

And then make sure those changes propagate to your kernel’s command-line at boot. This is where running reinstall-kernels might help.

Reboot and test it out

Restart your machine and then run the following to see if hibernation is configured correctly:

$ sudo systemctl hibernate