Convenient file system encryption on Linux

Year of the Linux Desktop

It will come soon; we just have to wait.
Everything will be free there, everything will be fantastic.
We’ll probably not even have to die.

– Yegor Letov

So, I wanted to make my Linux machine genuinely secure. I mean that even if someone gains physical access to the hardware, there must be no possibility of accessing the data and secrets stored on the disk. The usual solution is to employ at-rest disk encryption 1. This provides some security2, but how are you supposed to enable the decryption of your data?

Well, obviously, you need to enter the decryption passphrase. This is literally the only possibility provided by the Linux distributions I have checked (Fedora, Manjaro). So, have fun having to remember yet another long password and possibly having to enter it in a hostile environment. Have you ever used your laptop in public? You never know who is watching you or recording what you are doing.

This is just one small papercut, out of many yet to come, which you will encounter when trying to use Linux on a desktop. But, unfortunately, it seems that everyone just accepts such minor inconveniences without much afterthought because, hey, it’s Linux, it’s open source, it’s different, you just need to change your habits, and it will be ok, and so on.

In this article, we will see how to set up the Linux installation to be secure while at the same time making you forget you even have any encryption enabled. Do note that nothing here is truly new, but it’s also not readily available in any single place. You will have to sift through random wikis, blog posts, etc., including heaps of obsolete information. There is always another way to do something in Linux; nobody will tell you that you shouldn’t do it anymore…

System setup

Let’s start by going into the UEFI setup. There, let’s make sure legacy mode is not selected. Some machines may even no longer provide such an option (good riddance!).

Then enable the Trusted Platform Module module of the Windows 11 fame. Or maybe the Stallman’s “Treacherous Computing” fame. This module may be available as a separate physical component. Or it may be implemented by your CPU firmware under names such as Intel Platform Trust Technology or AMD fTPM. We will see why this is needed later on.

The last thing we will look at is Secure Boot. Oh no, it’s another Stallman’s disaster, what do? Well, for starters, let’s enable it. Now you probably can’t boot your Linux, whether it’s already installed or a bootable USB stick3. Doesn’t matter, as we don’t want to use the default signing keys anyways. Why should anyone else be able to decide what is authorized to run on my computer? As such, we need to clear the Secure Boot keys from the firmware. This will put the Secure Boot into Custom Mode, and we will have to finish its setup later. For the time being, it won’t enforce the signing requirements.

Be aware that for enhanced hilarity, some hardware may be unable to properly boot after you erase the default signing keys because it will require firmware to be loaded at boot time. That firmware is signed with a Microsoft key. Which is no longer there. You have been warned4.

Installing the distribution

I will be installing the Manjaro Linux with the default partition setup, enabled encryption, and btrfs as the file system. The resulting setup will require some adjustments after the installer finishes. Similar corrections may or may not be needed when another distribution is installed instead. Some day maybe there will be a distribution that handles everything correctly, but that day is not today.

Note that reading through all this should give you enough information to comfortably perform all the required steps in a clean way, for example, when doing a clean installation of Arch Linux.

But right now, back to Manjaro. When the installation finishes I don’t reboot but continue the work in the live installation environment. First, I need to do some cleanup. This is only required to clean up the state the installer leaves the system. If you would boot with the installation media later, there wouldn’t be a need to do so.

[manjaro ~]# umount /tmp/calamares-root-o9mphjm3/
[manjaro ~]# ls -l /dev/mapper/
total 0
lrwxrwxrwx 1 root root       7 Apr 22 01:08 luks-f466a527-9c72-4d07-84cd-540ea4717425 -> ../dm-0
[manjaro ~]# cryptsetup close luks-f466a527-9c72-4d07-84cd-540ea4717425

With this done, let’s see the partition setup created by the installer:

[manjaro ~]# fdisk -l /dev/nvme0n1
Disk /dev/nvme0n1: 465.76 GiB, 500107862016 bytes, 976773168 sectors
...

Device          Start       End   Sectors   Size Type
/dev/nvme0n1p1   4096    618495    614400   300M EFI System
/dev/nvme0n1p2 618496 976768064 976149569 465.5G Linux filesystem

Note that in your case, this may as well be /dev/sda or similar. Let’s also inspect the Linux Unified Key Setup responsible for encryption.

[manjaro ~]# cryptsetup luksDump /dev/nvme0n1p2
LUKS header information for /dev/nvme0n1p2

Version:        1

Key Slot 0: ENABLED
Key Slot 1: ENABLED

Here we see that there’s legacy LUKS version 1 in use, and two key slots are enabled. If you issue this command, you will see much more data, but I have removed all the irrelevant noise.

Fixing things up

Let’s start by migrating to an up-to-date version of LUKS.

[manjaro ~]# cryptsetup convert /dev/nvme0n1p2 --type luks2
Are you sure? (Type 'yes' in capital letters): YES
[manjaro ~]# cryptsetup luksDump /dev/nvme0n1p2
LUKS header information
Version:        2

Data segments:
  0: crypt
        offset: 2097152 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 512 [bytes]

Keyslots:
  0: luks2
        Key:        512 bits
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      pbkdf2
  1: luks2
        Key:        512 bits
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      pbkdf2

The aes-xts-plain64 cipher is the best choice, as it can be hardware accelerated. You can see the benchmark results of different available ciphers by running cryptsetup benchmark. However, the pdkdf2 function is less than ideal, so let’s change it to something more secure. While we’re at it, let’s also remove the second key slot, which purposes at this point are not exactly known (the first one stores the decryption password).

[manjaro ~]# cryptsetup luksChangeKey /dev/nvme0n1p2 --pbkdf argon2id
Enter passphrase to be changed:
Enter new passphrase:
Verify passphrase:
[manjaro ~]# cryptsetup luksKillSlot /dev/nvme0n1p2 1
Enter any remaining passphrase:
[manjaro ~]# cryptsetup luksDump /dev/nvme0n1p2
Keyslots:
  0: luks2
        Key:        512 bits
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      argon2id

All is fine here now.

Accessing the encrypted partition

Let’s now chroot to the freshly installed system. Notice that when opening the LUKS container, we also set up a couple of flags to improve SSD performance. Since the flag --persistent is used, this has to be done only once.

[manjaro ~]# cryptsetup open --allow-discards --perf-no_read_workqueue --perf-no_write_workqueue --persistent /dev/nvme0n1p2 luks
Enter passphrase for /dev/nvme0n1p2:
[manjaro ~]# mount /dev/mapper/luks /mnt -o subvol=@
[manjaro ~]# mount /dev/nvme0n1p1 /mnt/boot/efi/
[manjaro ~]# manjaro-chroot /mnt/

The first thing to do is to remove crypto_keyfile.bin from the root directory. This is because the second key slot was referencing this key. We don’t need it, as the slot was removed.

Bootloader

We begin by removing the GRUB bootloader5 and replacing it with systemd’s bootloader6.

[manjaro ~]# rm /crypto_keyfile.bin
[manjaro ~]# pacman -R grub grub-btrfs grub-theme-manjaro
[manjaro ~]# rm -r /etc/default/grub.pacsave /boot/grub/ /boot/efi/EFI/Manjaro/grubx64.efi
[manjaro ~]# efibootmgr
Boot0000* GRUB
Boot0001* UEFI : USB :  USB DISK 2.0 PMAP : PART 1 : OS Bootloader
Boot0002* Manjaro
[manjaro ~]# efibootmgr -B -b 0000
[manjaro ~]# efibootmgr -B -b 0001
[manjaro ~]# efibootmgr -B -b 0002
[manjaro ~]# bootctl install
Copied "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" to "/boot/efi/EFI/systemd/systemd-bootx64.efi".
Copied "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" to "/boot/efi/EFI/BOOT/BOOTX64.EFI".
Created EFI boot entry "Linux Boot Manager".

For security reasons, the boot loader should be configured to disallow changing its setup. You can do so by adding editor 0 to /boot/efi/loader/loader.conf.

Boot preparations

Now let’s tell the operating system how the encrypted data should be accessed. When the system is already running, this is done through the /etc/crypttab file. The installer may have filled this file with entries such as:

# <name>               <device>                         <password> <options>
luks-f466a527-9c72-4d07-84cd-540ea4717425 UUID=f466a527-9c72-4d07-84cd-540ea4717425     /crypto_keyfile.bin luks

This is no longer relevant for us (for one, the UUID is no longer valid after migration to LUKS2), and we don’t need to do any decryption after the root is mounted, so the entry should be removed. Instead, let’s create the /etc/crypttab.initramfs file with the following entry:

luks /dev/nvme0n1p2 - luks

Unlike the basic /etc/crypttab file, this second file will be placed in the boot package and will enable the kernel to access the system partition. Notice that a simple name, luks (not to be confused with the luks option at the end of the line), is used and that we’re referring to the disk device directly. Ok, now let’s fix the fstab entries. For example, we could have entries that currently look something like this:

UUID=3D78-7969                                        /boot/efi vfat  umask=0077 0 2
/dev/mapper/luks-f466a527-9c72-4d07-84cd-540ea4717425 /         btrfs subvol=/@,defaults,discard=async 0 0

This is what we want to have instead:

/dev/nvme0n1p1   /boot/efi      vfat    umask=0077 0 2
/dev/mapper/luks /              btrfs   subvol=/@,defaults,discard=async 0 0

A bit more readable, don’t you think? But we still have work to do. The next thing to provide is the kernel boot command line. Simply create a file at /etc/kernel/cmdline and enter the required arguments. For example, the following commands could be used. The most important thing is to provide the root disk device and the subvolume to use in case of btrfs.

quiet splash apparmor=1 security=apparmor udev.log_priority=3 rw root=/dev/mapper/luks rootflags=subvol=@

Boot payload

Now let’s set up the initial RAM filesystem. We will also have to prepare a unified kernel image, which bundles the kernel, its parameters, the RAM image (containing the cryptsetup.initramfs file), and so on. How you do this depends on the distribution. Here I will cover the mkinitcpio used by Arch and its derivatives. First, let’s edit the /etc/mkinitcpio.conf configuration file. There are two changes to be made there. Remove the crypto keyfile and change hooks to use the systemd path.

FILES=()
HOOKS="systemd autodetect modconf block keyboard sd-vconsole sd-encrypt filesystems"

Then there’s the kernel-specific preset, which may be in a different file for future kernels. In such a case, you will also have to make the same adjustments in these presets. For example, you may need to add the following lines to the /etc/mkinitcpio.d/linux515.preset file:

ALL_microcode=(/boot/*-ucode.img)
default_efi_image="/boot/efi/EFI/Linux/manjaro-linux.efi"

Having done that, you should generate the EFI image, which will be executed by the bootloader. With systemd-boot no additional configuration is needed. The kernel bundle will be automatically discovered.

[manjaro ~]# mkinitcpio -P
==> Building image from preset: /etc/mkinitcpio.d/linux515.preset: 'default'
==> Creating UEFI executable: /boot/efi/EFI/Linux/manjaro-linux.efi
  -> Using UEFI stub: /usr/lib/systemd/boot/efi/linuxx64.efi.stub
  -> Using kernel image: /lib/modules/5.15.32-1-MANJARO/vmlinuz
  -> Using cmdline file: /etc/kernel/cmdline
  -> Using os-release file: /etc/os-release
  -> Using microcode image: /boot/intel-ucode.img
==> UEFI executable generation successful

The system should now be bootable. Exit the chroot and reboot. Keep the decryption passphrase at hand, as you will need to enter it.

[manjaro ~]# umount -R /mnt
[manjaro ~]# reboot

If the boot process cannot complete, please remember that you can always use the installation media to mount the encrypted partition and chroot to fix the problems.

Securing the boot process

Recall that we have configured the Secure Boot to operate in Custom Mode but have not finished the setup. We will do it now using the sbctl utility, which automates most of the process. We just have to create our own signing keys and then enroll them to UEFI.

[manjaro ~]# pacman -S sbctl
[manjaro ~]# sbctl create-keys
Secure boot keys created!
[manjaro ~]# sbctl enroll-keys
Enrolled keys to the EFI variables!

Warning! At this point, the Secure Boot enforcement is enabled, and you will not be able to boot anymore!

Let’s take care of this minor inconvenience by verifying the signing status.

[manjaro ~]# sbctl verify
Verifying file database and EFI images in /boot/efi...
✗ /boot/efi/EFI/Linux/manjaro-linux.efi is not signed
✗ /boot/efi/EFI/boot/bootx64.efi is not signed
✗ /boot/efi/EFI/systemd/systemd-bootx64.efi is not signed

As you can see, two copies of the bootloader are present7, and also a system boot image which we created with the mkinitcpio command. Let’s sign everything.

[manjaro ~]# sbctl sign -s /boot/efi/EFI/Linux/manjaro-linux.efi
[manjaro ~]# sbctl sign -s /boot/efi/EFI/boot/bootx64.efi
[manjaro ~]# sbctl sign -s /boot/efi/EFI/systemd/systemd-bootx64.efi
[manjaro ~]# sbctl verify
Verifying file database and EFI images in /boot/efi...
✓ /boot/efi/EFI/Linux/manjaro-linux.efi is signed
✓ /boot/efi/EFI/boot/bootx64.efi is signed
✓ /boot/efi/EFI/systemd/systemd-bootx64.efi is signed

The boot images have been signed, and with the -s parameter, we have requested that they stay signed if they change in the future. For this to work, you will obviously need to ensure the signing process is executed, e.g., after the kernel package is updated. In Manjaro this is done automatically.

Lastly, let’s verify the Secure Boot status.

[manjaro ~]# sbctl status
Installed:      ✓ sbctl is installed
Setup Mode:     ✓ Disabled
Secure Boot:    ✓ Enabled

The Secure Boot is enabled, the signing keys are enrolled (as the setup mode is no longer active), and the boot images have been signed.

Storing decryption keys in TPM

Let’s now reboot to see if everything is all right. Oh, it still asks for the decryption key. Let’s fix this now.

By the way, do not skip this reboot. The setup steps described below will not work correctly without a proper Secure Boot state. Recall that the Secure Boot was still in setup mode when the previous boot was done, so the system was not booted securely.

We will use the systemd-cryptenroll command to review the encryption key slots. This is similar to what we previously saw with the cryptsetup command.

[manjaro ~]# systemd-cryptenroll /dev/nvme0n1p2
SLOT TYPE
   0 password

The only way to decrypt the system partition right now is with the password. We want to instead store the decryption key in the TPM chip, where it won’t be accessible to anyone. The TPM will release the decryption keys to the bootloader only if certain conditions are met. We may decide how we want to secure the TPM state by selecting the PCR registers which should be checked. For example, the PCR 0 register stores the UEFI firmware checksum and will change after the UEFI upgrade. The PCR 1 register stores the hash of UEFI settings and will change when some UEFI options are altered. The summary of all registers can be found at the Arch wiki.

While you can require any combination of registers, the sensible option is to check the Secure Boot state, which is stored in the PCR 7 register. This can be done with the following command:

[ymir ~]# systemd-cryptenroll /dev/nvme0n1p2 --tpm2-device=auto --tpm2-pcrs=7
🔐 Please enter current passphrase for disk /dev/nvme0n1p2: *********
New TPM2 token enrolled as key slot 1.
[ymir ~]# systemd-cryptenroll /dev/nvme0n1p2
SLOT TYPE
   0 password
   1 tpm2

Do note that the enrollment process saves the current status of the PCR registers. This is why performing a reboot after the Secure Boot was fully enabled was important.

Summary

The system boot process is now secure, and the decryption is automated. While it would be a good idea to have a password on UEFI firmware, it is not necessarily required. For example, suppose the attacker disables the Secure Boot enforcement. In that case, the TPM decryption keys will not release, and the system partition will not be decrypted without manual password entry, keeping your data secure.

If the boot images are changed, the signature will no longer be valid, and the system will fail to boot due to Secure Boot. And the signing keys are stored only on the encrypted partition, so it is impossible to sign the images without booting the system first.

Finally, the encrypted partition is only accessible if you enter the decryption password yourself or if the security checks have not failed.

Is it truly secure?

It is as secure as you make it. After the boot process is performed, the attack surface is much broader, as now we basically have to consider what the complete operating system is doing. Are there any undiscovered kernel bugs? Can the init process be interrupted? Are the login passwords secure?


  1. It is certainly interesting to consider what “the disk” means exactly. First, there’s the UEFI boot partition8, which cannot be encrypted, as the system has to start from somewhere and load the decryption procedures. Then we have the root partition, possibly the swap partition, maybe also a home partition, and so on. Since everything has to be encrypted9, we must consider how the decryption should be handled. First, each partition can be encrypted separately, with its own password. But the more convenient way of doing things is to create an encrypted logical device, which can then be partitioned. You can do so with the Logical Volume Manager, which has the added bonus of hiding the internal partitioning scheme from the external observer. Alternatively, you may use btrfs file system, which has support for subvolumes. Or just use a single root partition for everything because it’s not the nineties anymore. ↩︎

  2. Well, evil maid attacks are a thing, and just the encryption alone will not help you in any way if some part of the boot process can be poisoned without your knowledge. What if the attacker modifies the decryption software to transmit the decryption password you have entered? ↩︎

  3. Unless the distribution authors have paid a hefty fee to Microsoft to have the kernel signed10. Fedora and Ubuntu sign their kernels, so they should be bootable in the default configuration, but you shouldn’t expect the smaller distros to do so. ↩︎

  4. For what it’s worth, the sbctl utility may be able to detect such situations and prompt you to enroll the required but missing Microsoft keys before putting Secure Boot in effect. ↩︎

  5. GRUB is a relict of the past that should have been put to rest long ago. It was fine as a LILO replacement 25 years ago, but now it can’t handle text output satisfactorily. On multiple machines, I have seen GRUB redraw the text screen contents line by line. Let it die. ↩︎

  6. If you think systemd is the devil personified, then, well… I’m sure you will figure out something more appropriate for your setup. ↩︎

  7. The first copy is explicitly registered in the UEFI boot entry table. You may have multiple bootloaders registered, and they will be visible and named in the UEFI boot selection menu. But, if you temporarily remove the disk, the boot entries may be flushed, as they would no longer be valid. In such a case, there needs to be a default bootloader on the disk, just like a default bootloader on an installable USB media. For this purpose, a second bootloader copy was installed with a generic name that is recognized even if it is not present in the boot table. ↩︎

  8. If you’re one of these legacy BIOS weirdos, please return to your cave. ↩︎

  9. This includes the swap space unless you want random fragments of the memory to be visible to anyone. ↩︎

  10. Why does Microsoft have to sign the Linux kernel? Is it because they are an evil corporation that wants to control your computer? Well, maybe, but in this case, it’s because each PC has to be able to boot Windows. And why is that? Because for decades the Linux Desktop has been a running joke, and it still is. ↩︎