Encrypted Root with LUKS and Opal
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.
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
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