Encrypted BTRFS Arch Installation

Posted on Sun 09 June 2024 in /hacks

I have recently had to do a re-install for my laptop (technically netbook) and I was interested in the 'easy snapshotting' available with BTRFS, so this is a quick guide to how I set my netbook.

First I used the latest arch ISO written to a USB drive, and booted into the live environment. To check if your live environment was booted into UEFI mode (desired), you can read the contents of the fw_platform_size.

cat /sys/firmware/efi/fw_platform_size

This should read 64 ideally, if this does not exist try going through the motherboard bios to see if UEFI is available for your motherboard. If the value is 32, then... go look somewhere else. Although most of this guide is identical for legacy BIOS boot.

As I use a UK keyboard, set the keyboard layout

loadkeys uk

I am using a USB-Ethernet network interface that has a driver in the Linux kernel, so I am able to access the internet. If you are using Wi-Fi, iwctl comes pre-installed in the Arch live boot environment, and is easily configured.

I prefer to set up the GUID Partition Table (GPT) with gdisk instead of fdisk and my disk is a NVMe drive so appears as /dev/nvmen0 instead of /dev/sda for example.

gdisk /dev/nvmen0

First we want to create a new GPT

Command (? for help): o

Now to create our partitions. I will have 3 partitions; an EFI partition (FAT32), a boot partition (unencrypted EXT4) and an encrypted root partition (BTRFS). The EFI partition does not need to be big, but it is recommended at least 100MB, so I chose 128MB and to bring the first two partitions to a round 1GB cumulatively, I chose my boot partition to be 896MB leaving the remaining for my root partition. The only other important detail to note here, is that the EFI flag must be EF00, the rest can be default (8300).

Command (? for help): n
Partition number (1-128, default 1): [default]
First Sector (34-xxxxx, default = 2048) or {+-}size{KMGTP}: [default]
Last Sector (2048-xxxxx, default = xxxxx) or {+-}size{KMGTP}: +128M
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): EF00

Command (? for help): n
Partition number (2-128, default 2): [default]
First Sector (34-xxxxx, default = 264192) or {+-}size{KMGTP}: [default]
Last Sector (264192-xxxxx, default = xxxxx) or {+-}size{KMGTP}: +896M
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): [default]

Command (? for help): n
Partition number (2-128, default 3): [default]
First Sector (34-xxxxx, default = 2099200) or {+-}size{KMGTP}: [default]
Last Sector (2099200-xxxxx, default = xxxxx) or {+-}size{KMGTP}: [default]
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): [default]

Now you can review the new table

Command (? for help): p

Once you are happy with the partition table, you can write it with:

Command (? for help): w
Do you want to proceed? (Y/N): Y

Now we have a partition table, we can create the filesystems on top of them. First lets start with the EFI partition, which must be FAT32:

mkfs.fat -F32 -n EFI /dev/nvme0n1p1

Again, if you are not using NVMe drive, this will be something else, but ensure you don't mix up your partitions! Boot partition is also very straight forward, but the filesystem is going to be EXT4. There are some legacy excuses to use EXT2 instead due to the lack of journaling, but this is outdated advice and I would recommend EXT4.

mkfs.ext4 /dev/nvme0n1p2

Now the EFI and boot partition are set up, we can start with the encrypted root file system. Create the encryption layer (you can specify the type i.e. LUKS2 but LUKS2 is the default):

cryptsetup luksFormat /dev/nvme0n1p3

Use a key that is memorable but secure. Alternatively you can generate a binary file from /dev/urandom and use that as a key, but ensure to copy this on to USB to avoid being locked out of your drive.

Now we can open the encryption:

cryptsetup open /dev/nvme0n1p3 crypt_root

I chose the name crypt_root, but this could be anything, but what ever name is chosen, must be used later in setup. Now we can create our BTRFS on this encrypted volume.

mkfs.btrfs /dev/mapper/crypt_root

And I chose 6 BTRFS subvolumes:

  • @ for /, the root filesystem (this is the one that will be snapshotted),
  • @home for /home containing user files,
  • @snapshots for /.snapshots which will contain my snapshots,
  • @log for /var/log, no point it snapshotting logs,
  • @pkg for /var/cache/pacman/pkg the cache for pacman can get large,
  • @swap for /.swap/ for containing my swap files.

I prefer using swap files than swap partitions as they offer the same advantages, but are also easier to resize. Of course, we can't have a swap file in a partition that will be snapshotted, so I create a new partition to house my swap files (in /.swap).

But first we will mount this BTRFS partition

mount /dev/mapper/crypt_root /mnt

And create the subvolumes:

btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@snapshots
btrfs subvolume create /mnt/@log
btrfs subvolume create /mnt/@pkg
btrfs subvolume create /mnt/@swap

Now unmount the partition and remount with the BTRFS flags

mount -o compress=zstd,noatime,space_cache=v2,subvol=@,discard=async /dev/mapper/crypt_root /mnt
mount --mkdir -o compress=zstd,noatime,space_cache=v2,subvol=@home,discard=async /dev/mapper/crypt_root /mnt/home
mount --mkdir -o compress=zstd,noatime,space_cache=v2,subvol=@snapshots,discard=async /dev/mapper/crypt_root /mnt/.snapshots
mount --mkdir -o compress=zstd,noatime,space_cache=v2,subvol=@log,discard=async /dev/mapper/crypt_root /mnt/var/log
mount --mkdir -o compress=zstd,noatime,space_cache=v2,subvol=@pkg,discard=async /dev/mapper/crypt_root /mnt/var/cache/pacman/pkg
mount --mkdir -o compress=zstd,noatime,space_cache=v2,subvol=@swap,discard=async /dev/mapper/crypt_root /mnt/.swap

And add the other partitions

mount --mkdir /dev/nvmen0p2 /mnt/boot
mount --mkdir /dev/nvmen0p1 /mnt/boot/efi

Now we have all the partitions and subvolumes mounted, we can install Linux on this partition, for Arch Linux we can use pacstrap:

pacstrap -K /mnt base linux linux-firmware neovim cryptsetup btrfs-prog grub-btrfs snapper grub-x86_64-efi efibootmgr bash-completion

Swap neovim with your editor of choice.

We can use genfstab packaged with the Arch ISO. Alternatively you can construct the /etc/fstab yourself with the mount commands above. I am using -U to use UUID instead of -L for labels.

genfstab -U /mnt >> /mnt/etc/fstab

This does a pretty good job, but it contains the subvolid=xxx, remove these from the mount parameters for each of the BTRFS partitions.

If you are not installing Arch Linux and are using normal chroot, you must first bind mount /dev, /dev/pts, /proc, /sys from the host system to the one to chroot in (mount --bind /dev /mnt/dev, etc.).

arch-chroot /mnt

Now inside the new system, we can start by setting the time

ln -sn /usr/share/zoneinfo/Europe/London /etc/localtime
hwclock --systohc

Of course, replace the zone info with your own region and city. Now you can generate localisation, key map and hostname:

locale-gen
echo "LANG=en_GB.UTF-8" > /etc/locale.conf
echo "KEYMAP=uk" > /etc/vconsole.conf
echo "netbook" > /etc/hostname

Of course, this is all configuration, so yours will likely be different.

To set up a 15GB swap file.

dd if=/dev/zeros of=/.swap/swapfile bs=1M count=15360
mkswap /.swap/swapfile
swapon /.swap/swapfile

Now add it to your /etc/fstab

/.swap/swapfile none swap defaults,x-systemd.after=/.swap

I have added the x-systemd.after=/.swap, which is systemd dependent, but this should really be fine without it.

Your UUID for the encrypted disk can be retrieved with:

blkid /dev/mapper/crypt_root -sUUID -ovalue

Now we can edit the grub and mkinitcpio files for our encrypted BTRFS system. Edit /etc/default/grub to give initramfs the knowledge of the encrypted device replacing myuuid with the one given with the command above.

GRUB_CMDLINE_LINUX="root=/dev/mapper/crypt_root cryptdevice=UUID=myuuid:crypt_root"    
GRUB_ENABLE_CRYPTODISK=y

Now update /etc/mkinitcpio.conf to load the modules required.

MODULES=(btrfs)
HOOKS=(base udev autodetect modconf kms keyboard encrypt keymap consolefont block filesystems fsck grub-btrfs-overlayfs)

As seen above, I have opted out of using systemd specific modules. This should mean that this should work independent of the chosen init system.

After setting the config files, we can update/generate the binaries:

mkinitcpio -P
grub-install --target=x86_64-efi --bootloader-id=GRUB --efi-directory=/boot/efi
grub-mkconfig -o /boot/grub/grub.cfg

Now your bootloader (grub) and initramfs are installed, you are able to reboot into your new installation!