A Barbarian’s Guide to Full-Disk Encryption with Arch Linux

Tags: linux, howtos, security

Published on
« Previous post: LLMs Are Proof That Unix Won

Even after more than two decades of using Linux, I still do not feel like a venerable graybeard. While I am phenotypically getting there—tempus fugit and all that—I feel that my use of Linux as a daily driver is not refined, but more akin to that of a barbarian of the Dungeons & Dragons kind. In this spirit, I want to provide some of my own notes for setting up a new system with full-disk encryption. Let’s first dispense with some totally-not-contrived questions:

  1. What? We are setting up Arch Linux on a new computer. We will make sure that its hard drive is encrypted. To decrypt it, we have to enter a passphrase at boot or resume time.

  2. Why? Devices get lost. If your data is encrypted, you only have to worry about small stuff like extinction-level events as opposed to having to think that someone gains access to your precious data.

  3. How? We will use LUKS, the Linux Unified Key Setup, to set up an encrypted swap partition and a root partition with a btrfs filesystem. Except for typing the passphrase for booting the system, you will not feel that the encryption is there.

  4. But? Look, this is how I like to use the system. Remember that I am a barbarian. I have heard some people talk about alternatives to LUKS but they used long, complicated words. I know LUKS. It works. We will use LUKS. I also know that there are other filesystems. I do not care. We will use btrfs.1

Prerequisites

I am assuming that you managed to boot your computer from an Arch Linux installation medium. My recommendation is to always get a new ISO image since chances are higher that all required drivers are present. It will also simplify our bootstrapping later on.

Your next step is to get a WiFi or network connection working somehow. The Arch Wiki can help; I am not covering it here because this step depends so much on your specific hardware.

Setting up the Partitions

A word of caution: This is a barbarian’s guide, so we are not going to do anything like a fancy dual-boot setup. It is certainly possible to also do full-encryption of the Arch partition in such a context, but I want to keep things simple. If you absolutely have to do dual-boot, make sure to install Windows first because it may could will definitely destroy your bootloader otherwise. (You want to dual-boot Windows, right? It’s always Windows.)

In what follows, I will assume that your hard drive is represented as /dev/nvme0n1. Make sure to check your drives with lsblk before you proceed with the setup. If you are sure, we can now proceed to create the partitions via fdisk. Make sure you have a GPT (GUID Partition Table). This should be the default but you can get a new one via g at the fdisk prompt. I always create the following three partitions:

  1. An EFI partition with 1 GiB.
  2. A Linux partition with ~90% the size of the hard drive.
  3. A swap partition that is slightly larger than the amount of my RAM.

None of these choices are particularly smart or principled; except for the EFI one, which is very generous in terms of its size. After creating the partitions via n at the fdisk prompt, your partition table should look like this:

Disk /dev/nvme0n1: 953.87 GiB, 1024209543168 bytes, 2000409264 sectors
Disk model: SAMSUNG MZVL21T0HCLR-00BL7
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 28538CBB-AAE0-40F4-8015-B90B4C6D9F1A

Device              Start        End    Sectors  Size Type
/dev/nvme0n1p1       2048    2099199    2097152    1G EFI System
/dev/nvme0n1p2    2099200 1910507519 1908408320  910G Linux filesystem
/dev/nvme0n1p3 1910507520 2000408575   89901056 42.9G Linux swap

Save this table via w and quit fdisk using q.

Setting up the Filesystems

We will create an encrypted root and swap partition:

  1. Use cryptsetup -v luksFormat /dev/nvme0n1p2 to prepare the root partition. This command prompts for a passphrase. Make sure to remember it and type it correctly. If you are not a barbarian, feel free to fiddle with the options of cryptsetup; but for me, the default options are okay and I see no reason for changing them. Or, to put it a bit bluntly: My setup is supposed to deter criminals, not Mossad. If you want to deter Mossad, you are going to have a bad time. To quote James Mickens:

    If your adversary is the Mossad, YOU’RE GONNA DIE AND THERE’S NOTHING THAT YOU CAN DO ABOUT IT.

  2. Do the same thing for the swap partition, i.e., /dev/nvme0n1p3.

  3. Open the root partition via cryptsetup open /dev/nvme0n1p2 root. This an excellent test of your ability to type your passphrase. If you are a barbarian like me, you will need multiple attempts.

  4. Turn this into a btrfs filesystem via mkfs.btrfs /dev/mapper/root.

  5. Open the swap partition with cryptsetup open /dev/nvme0n1p3 swap.

  6. Turn this into a swap area via mkswap /dev/mapper/swap and enable it via swapon /dev/mapper/swap.

  7. Mount the root partition with mount /dev/mapper/root /mnt.

  8. Create btrfs subvolumes for the root filesystem and your home directory, respectively:

    btrfs subvolume create /mnt/@      # root
    btrfs subvolume create /mnt/@home  # home
    

    This will enable you to use the btrfs snapshot functionality if you so desire. If not, it will just mean that you have to type a couple of commands more. Notice that I am not creating a subvolume for /var/log or some related directories because I do not intend to use this one specifically for any snapshots; if anything, I’ll only do snapshots of my home directory.

    You can also add additional subvolumes later so you are not losing out on anything here.

  9. Unmount the root filesystem via umount /mnt.

Bootstrapping

Having created the basic filesystem layout, it is now time to bootstrap the system and install some packages:

  1. Mount the root partition via mount -o subvol=@ /dev/mapper/root /mnt.

  2. Create an additional mount point for the home directories and mount it:

    mkdir /mnt/home
    mount -o subvol=@home /dev/mapper/root /mnt/home
    
  3. Create a new partition for the bootloader via mkfs.fat /dev/nvme0n1p1. Warning: This will delete your existing bootloader so if you know what you are doing and what to have a dual-boot system, do not follow this guide blindly.2

  4. Create an additional mount point for the bootloader and mount it:

    mkdir /mnt/boot
    mount /dev/nvme0n1p1 /mnt/boot
    

    This partition is not encrypted, and I am perfectly okay with it.

  5. Install the most relevant packages via pacstrap /mnt base base-devel linux linux-firmware btrfs-progs git neovim man-db man-pages texinfo intel-ucode iwd sudo, for instance. You can add as many additional packages as you like but I personally find that these get me started reasonably well.

    Notice: If you are using an AMD processor, you should install amd-ucode instead of intel-ucode. Moreover, you can save a couple of MiB by picking specific packages from the linux-firmware collection. I honestly do not find that it makes a big difference and I would rather save some time here.

  6. Generate an fstab with genfstab -U /mnt >> /mnt/etc/fstab. You should check this one afterwards. Mine looks like this:

    # Static information about the filesystems.
    # See fstab(5) for details.
    
    # <file system> <dir> <type> <options> <dump> <pass>
    
    # /dev/nvme0n1p1
    UUID=DCF1-BACA          /boot           vfat            rw,relatime,fmask=0137,dmask=0137,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro   0 2
    
    # /dev/mapper/root
    UUID=8a63a764-702c-4195-a9c1-b868b5b984d0       /               btrfs           rw,relatime,ssd,space_cache=v2,subvol=/@        0 0
    
    # /dev/mapper/root
    UUID=8a63a764-702c-4195-a9c1-b868b5b984d0       /home           btrfs           rw,relatime,ssd,space_cache=v2,subvol=/@home    0 0
    
    # /dev/mapper/swap
    UUID=dc1db2b2-fceb-4c66-b599-88d9b3e1f5e8       none    swap    defaults        0 0
    

    If necessary, you can correct any errors by using blkid and checking the UUIDs. Pay particular attention to the fact that you are not supposed to use the UUID of the device per se but the UUID of the mapper instead. Here is what blkid is printing on my system, for example:

    /dev/nvme0n1p1: UUID="DCF1-BACA" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="97014ff5-fa07-4295-aff5-fb8141cea5a4"
    /dev/nvme0n1p2: UUID="01b2baa1-33ea-46c9-8760-24f7afd40ae5" TYPE="crypto_LUKS" PARTUUID="5e078747-0209-4363-a472-ae98797023b2"
    /dev/nvme0n1p3: UUID="beb62149-652f-4988-9787-f54cd2ec188d" TYPE="crypto_LUKS" PARTUUID="cadef158-5bc8-454f-91c4-4f78268ac790"
    /dev/mapper/root: UUID="8a63a764-702c-4195-a9c1-b868b5b984d0" UUID_SUB="ffa67280-ba7f-455e-9201-ada132616ba7" BLOCK_SIZE="4096" TYPE="btrfs"
    /dev/mapper/swap: UUID="dc1db2b2-fceb-4c66-b599-88d9b3e1f5e8" TYPE="swap"
    

    You can see that only the /dev/mapper entries matter for root and swap, respectively, whereas /boot gets the regular partition UUID.

  7. Finally, start the chroot with arch-chroot /mnt.

Inside the chroot

Inside our cozy chroot, we can start making the system our own. None of this is strictly necessary for the encryption that I want to present in this post but a small digression cannot hurt.

  1. Set timezone via ln -sf /usr/share/zoneinfo/$REGION/$CITY /etc/localtime, where $REGION and $CITY are kind of near you. For instance, I have Europe and Zurich, respectively. Look at hwclock --systohc if you want to sync the system and hardware clocks.

  2. Uncomment your desired locales in /etc/locale.gen and run locale-gen.

  3. Set the desired locale in /etc/locale.conf. See, this is why it’s great to have nvim installed!

  4. Set a nice hostname in /etc/hostname.

  5. Set a root password via passwd.

Bootloader Setup

This is where it gets dicey because anything you do wrong here will require you to reboot from your USB device, decrypt the root partition again, mount it, and go to the chroot again—it is not like you will brick your system but it can feel annoying.

We will first make sure that our kernel is always compiled with support for encrypted filesystems and btrfs. Hence, edit /etc/mkinitcpio.conf and ensure the following lines are there:

BINARIES=(/usr/bin/btrfs)
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)

Warning: The order of the hooks matters, so do not experiment too much here. With this configuration, you will be able to decrypt multiple devices (root and swap).

After editing this, generate a kernel via mkinitcpio -p linux. If this fails, something weird is going on and you should not continue until you know what you are doing. Because booting without a kernel just gets you back to square one…

Assuming that everything worked, let’s install a bootloader via bootctl --variables=yes install. In my own configuration, I had to update fstab for the /boot partition to contain the fmask=0137,dmask=0027 options. If this is the case for you, you need to unmount and mount /boot, leaving the chroot once.

Bootloader Configuration

Finally, the configuration of the bootloader awaits!

  1. Create a file /boot/loader/entries/archlinux.conf with the following content:

    title Arch Linux
    linux /vmlinuz-linux
    initrd /intel-ucode.img
    initrd /initramfs-linux.img
    options rd.luks.name=01b2baa1-33ea-46c9-8760-24f7afd40ae5=root root=/dev/mapper/root rw rootflags=subvol=@
    options rd.luks.name=beb62149-652f-4988-9787-f54cd2ec188d=swap resume=/dev/mapper/swap
    

    Notice that you can use blkid idea to get the UUIDs. This time, make sure to use the actual device UUIDs (see above), which kind of makes sense since these are the things the kernel sees before any decryption can happen. The two lines with options will thus specify the respective devices and ensure that both root and swap are functional (meaning that hibernation will should work).

  2. Finally, edit /boot/loader/loader.conf to contain at least the following lines:

    default archlinux.conf
    timeout 3
    

    This instructs the bootloader to boot your Arch Linux entry after three seconds. Feel free to adjust additional settings here.

  3. Exit the chroot using CTRL + D. Then execute reboot and pray.

Summary

This all seems very involved, I know. But you get a lot—in essence, you will likely never have to touch this again. It just works (famous last words) and, thanks to the kernel configuration, automatically survives updates. On top of that, you raise the bar just a little bit and make it harder for someone to extract data from your device if they ever lay their hands on it. Whether this level of paranoia is necessary is up to you to decide, but since the couple of extra steps do not take long, I have been setting my system with variations of this recipe for some time now. And at the risk of bringing the wrath of graybeards, things have become so much easier thanks to the systemd hooks. No more /etc/crypttab shenanigans, no more hibernation issues, and a simple boot loader configuration. Truly wondrous!

And while I cannot guarantee that this is the perfect configuration, I can guarantee that it works for me. If you use an LLM to generate a similar configuration, it may make the following false claims:

  1. Claim: Multiple options lines are not allowed for the bootlader. This is wrong. The options will simply be concatenated. If you are worried about this, you can also just put everything into one long options line.
  2. Claim: You have to enter your passphrase twice. This is only true if you use different passphrases for the root and swap partition. Otherwise, the passphrase will be cached and unlocking works.

See? Truly wondrous!

Here’s to encrypted systems, until next time.


  1. Many of the steps are not specific to btrfs so I am sure you can figure it out. ↩︎

  2. You should in any case make use of your INT and WIS stats. I am just a barbarian, remember? ↩︎