This tutorial is written for my own future reference, but I’m also posting it online in case it helps someone else looking for a similar setup. While the first post on this series already laid out my specs, motivations and technological choices, this one will focus on implementing the core parts of my personal subsystem: an encrypted Arch Linux installation on top of Btrfs, with RAID1.

0. Overview

Here’s a sketch of what I’m aiming for:

sketch-arch

During the previous part of this series, I’ve used the Ubuntu 24.04 LTS guided installer to create an ESP partition (sda1, in green) and the core parts of my work subsystem (sda4 and sda5, in orange). This post will set up the remaining SSD partitions, as well as the HDD RAID1 mirror.

I’ll follow the general structure of the Arch wiki’s installation guide, with a few added comments and highlighting deviations from the standard route for my customizations.

1. Pre-installation

1.1. Acquire an installation image

Just download the latest ISO image from the official Arch download page.

1.2. Verify image hashes

Make sure the ISO’s sha256sum matches the expected hash (1b4a04ef8a7350852a13070ee498442b087a607a18840b4dd7d99867eb5f6a4c for 2024.05.01).

1.3. Prepare installation medium

Flash the image on a USB stick.

1.4. Boot the live environment

You’ll find yourself logged as root in a Zsh shell. These pacakges come pre-installed.

arch-tty
Arch install live environment, after a few sanity checks.

1.5. Set the console keyboard

Nothing to do here, since I use a US keyboard layout.

1.6. Verify the boot mode

cat /sys/firmware/efi/fw_platform_size should read 64.

1.7. Connect to the internet

Just use ethernet. The live system connects automatically using DHCP.
Make sure your connection is actually working by pinging an external server.

1.8. Update the system clock

Check if the output of timedatectl looks sane (in UTC+0).

1.9. Partition the disks

This is where it gets interesting. I already know how I want to partition my disks, but here are a few highlights copied straight from the Arch wiki:

  • Take time to plan a long-term partitioning scheme to avoid risky and complicated conversion or re-partitioning procedures in the future.

Done.

  • The following partitions are required for a chosen device: one partition for the root directory /; for booting in UEFI mode: an EFI system partition.
  • If the disk from which you want to boot already has an EFI system partition, do not create another one, but use the existing partition instead.
  • Swap space can be set on a swap file for file systems supporting it.

We already have an EFI partition created by the Ubuntu installer, so we’ll keep using that. Then, instead of a single root partition mounted at /, we’ll create separate root (/) and boot (/boot/) partitions, since we want the former to be encrypted (and the latter can’t be, depending on the second-stage boot loader we end up using). Finally, we’ll use a swap file since that’s supported on our filesystem of choice, Btrfs.

Aligning partitions correctly avoids excessive read-modify-write cycles. A typical practice for personal computers is to have each partition’s start and size aligned to 1 MiB (1 048 576 bytes) marks. This covers all common page and block size scenarios, as it is divisible by all commonly used sizes - 1 MiB, 512 KiB, 128 KiB, 4 KiB, and 512 B. Warning: Misaligned partitions will prevent being able to use 4096 B sectors with LUKS.

This important disk performance tip was hidden away in the Advanced Format page. Even though most disk partitioning tools default to MiB alignment, I think this should be highlighted in the partitioning step of the installation guide.

If you want to create any stacked block devices for LVM, encryption or RAID, do it now.

1.9.1. Securely erasing a drive

I’m going to err on the side of caution and securely erase my entire HDD:

wipefs -a /dev/sdb
cryptsetup open --type plain -d /dev/urandom --sector-size 4096 --cipher aes-xts-plain64:sha256 --key-size 256 /dev/sdb to_be_wiped
dd if=/dev/zero of=/dev/mapper/to_be_wiped status=progress bs=1M
cryptsetup close to_be_wiped

Here I’ve manually specified the fastest (according to a cryptsetup benchmark) combination of cipher and key size, as well as the physical sector size of my HDD (obtained with lsblk -t).

1.9.2. Physical partitioning

This is what our disks looked like after our encrypted Ubuntu 24.04 installation:

ubuntu-partition-layout
Disk layout after the Ubuntu install. sda is an SSD and sdb a bigger HDD.

Everything on the SSD is already MiB aligned. You can check that manually by running fdisk -l and making sure the start sector and total sectors assigned to each partition are divisible by 8. Then, I’ll just wipe the placeholder ext2 file systems which I used to “hide” available disk space from the Ubuntu installer:

wipefs -a /dev/sda2
wipefs -a /dev/sda3

As for the just-erased HDD, I’ll use fdisk’s interactive shell to:

  • Set up a fresh GPT partition table (g)
  • Create a new partition the exact same size of /dev/sda3 (n)
  • Commit my changes (w)

1.9.3. RAID on hybrid storage

We’ll now set up software RAID1 using mdadm. In my particular case, I’ll have a hybrid RAID1 setup: one of the mirrors is in an SSD, while the other is in an HDD. Thankfully, mdadm has a “write-mostly” flag which can mark expected-to-be-slower devices (/dev/sdb) in the array in order to make most reads come from the faster device (/dev/sda). Setting it up is as simple as:

mdadm --verbose --create --metadata=0.90 /dev/md0 --level=1 --raid-devices=2 /dev/sda3 --write-mostly /dev/sdb1

Before continuing, watch /proc/mdstat to make sure the array had enough time to resync.

arch-mdadm
Setting up software RAID on Linux is easier than I imagined.

1.9.4. LUKS encryption

Setting up dm-crypt / LUKS encryption on top of a RAID1 array is also quite simple:

cryptsetup -v luksFormat /dev/md0
cryptsetup open --allow-discards /dev/md0 arch-root
arch-cryptsetup
Save for discards, the default LUKS settings in this version of cryptsetup are exactly what I want for this particular system.

1.10. Format the partitions

As planned, I’ll use ext4 for my boot partition and Btrfs (on LUKS, on RAID) for the rest:

mkfs.ext4 /dev/sda2
mkfs.btrfs /dev/mapper/arch-root

1.11. Mount the file systems

I’ll start by temporarily mounting my Btrfs root at /mnt.

mount -o defaults,compress=zstd:1,ssd,discard=async,noatime /dev/mapper/arch-root /mnt
  • Compression is one of the reasons I chose Btrfs, and after looking at a few benchmarks, it seems that zstd:1 is the best compromise on compression ratio and CPU usage.
  • Apparently, Btrfs does not add ssd or discard options by default when it is mounted on top of LUKS/RAID, so I’ll add those manually.
  • Finally, I use noatime in order to avoid unnecessary writes on the hybrid RAID1 setup, since they’ll have HDD speeds and I want to preserve SSD read performance.

Now, since the filesystem is empty, this is probably the most convenient moment to set up other Btrfs subvolumes. I’ll follow the suggested filesystem layout for Snapper, with separate subvolumes @ and @home for / and /home respectively, then @var_log to exclude /var/log from root snapshots (note: I may have to create more subvolumes in the future in order to have separately-snapshottable containers, databases, and the like).

btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@var_log

Now, in order to actually use the @ subvolume as our / mountpoint (as opposed to mounting / directly on the root subvolume a.k.a. subvolid=5), we’ll unmount the current root:

umount /mnt

And then set up mountpoints (subvolumes and normal partitions alike) in the way they’re actually going to be used in the installed system, in hierarchical order:

mount -o subvol=@,defaults,compress=zstd:1,ssd,discard=async,noatime /dev/mapper/arch-root /mnt/

mkdir -p /mnt/home
mount -o subvol=@home,defaults,compress-force=zstd:1,ssd,discard=async,noatime /dev/mapper/arch-root /mnt/home

mkdir -p /mnt/var/log
mount -o subvol=@var_log,defaults,compress=zstd:1,ssd,discard=async /dev/mapper/arch-root /mnt/var/log

mkdir -p /mnt/boot
mount /dev/sda2 /mnt/boot

mkdir -p /mnt/boot/efi
mount /dev/sda1 /mnt/boot/efi
arch-btrfs
Setting up mount points for subvolumes and partitions alike.

2. Installation

2.1. Select the mirrors

reflector -p https -l 100 -f 20 --save /etc/pacman.d/mirrorlist

2.2. Install essential packages

This is where we decide what packages we want in our initial install:

pacstrap -K /mnt \
  base less which \
  linux-zen linux linux-firmware \
  intel-ucode \
  e2fsprogs dosfstools cryptsetup lvm2 mdadm btrfs-progs \
  networkmanager bind whois wget curl \
  nano vim \
  man-db man-pages \
  refind efibootmgr mkinitcpio \
  base-devel cmake git python gnupg openssh gdb \
  tree xxd htop perl-image-exiftool

3. Configure the system

3.0. Update RAID config file

Since we’re using RAID, we need to update the default mdadm configuration file:

mdadm --detail --scan >> /mnt/etc/mdadm.conf

Then open it on a text editor to make sure it looks OK.

3.1. Fstab

Similarly, for the fstab file:

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

3.2. Chroot

Now, we’ll be chrooting into the new system:

arch-chroot /mnt

3.3. Time

If you want the system’s local time to be in your own timezone instead of UTC:

ln -sf /usr/share/zoneinfo/$REGION/$CITY /etc/localtime
hwclock --systohc

3.4. Localization

Edit /etc/locale.gen and uncomment en_US.UTF-8 UTF-8, as well as any other needed UTF-8 locales. Then, generate the locales and set the system locale with:

locale-gen
echo 'LANG=en_US.UTF-8' > /etc/locale.conf

3.5. Network configuration

Create a hostname file:

echo 'my-cool-hostname' > /etc/hostname

And, in my case, I’ll manually enable the NetworkManager systemd unit:

ln -s /usr/lib/systemd/system/NetworkManager.service /etc/systemd/system/multi-user.target.wants/

3.6. Initramfs

In this setup, where I’m using LUKS encryption and software RAID, I needed to add mdadm_udev and encrypt to my initramfs hooks in /etc/mkinitcpio.conf:

HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block mdadm_udev encrypt filesystems fsck)

Then, in order to actually apply these changes and recreate the initramfs image:

mkinitcpio -P
arch-chroot
Note: the mkinitcpio command may issue some warnings while generating the initramfs; I just ignored the ones I got.

3.7. Root password

We can then set the root password with passwd.

3.8. Boot loader

In my case, I’ll use rEFInd (which I already installed in the pacstrap command):

refind-install
rm /boot/refind_linux.conf
cp /boot/efi/EFI/refind/refind.conf /boot/efi/EFI/refind/refind.conf.bak

After creating a backup of the original config, I edited /boot/efi/EFI/refind/refind.conf to reduce the boot timeout, to add support for the naming scheme of Arch Linux kernels and to enable the inclusion of a separate manual.conf config file.

# refind.conf
...
timeout 10
...
extra_kernel_version_strings "linux-hardened,linux-rt-lts,linux-zen,linux-lts,linux-rt,linux"
...
include manual.conf

Then, I like to set up a manual boot stanza in /boot/efi/EFI/refind/manual.conf, which reads:

# manual.conf
menuentry "Arch Linux" {
	icon    /EFI/refind/icons/os_arch.png
	volume  "8B2310D6-C36B-4FB9-929B-F2FF5D5B120D"
	loader  /vmlinuz-linux-zen
	initrd  /initramfs-linux-zen.img
	options "cryptdevice=UUID=6fd4672b-5745-404d-a4f2-5cf3984c0ae5:arch-root:allow-discards root=UUID=07a47b01-b163-4e7f-99ab-4c6310c4e4a1 rootflags=subvol=@ rw"
	submenuentry "Boot using fallback initramfs" {
		initrd /initramfs-linux-zen-fallback.img
	}
}
  • We need to specify the icon token before choosing a boot volume.
  • The volume token is set to the PARTUUID of my /boot partition (/dev/sda2).
  • loader and initrd paths are set using the specified volume as a root, so we don’t need to prefix those with /boot.
  • The cryptdevice kernel option must be set with the UUID of a block device of TYPE="crypto_LUKS", which corresponds to my software RAID block device.
  • The UUID in the root kernel option should match the / entry in my fstab.
  • Since my root filesystem is mounted in a non-default Btrfs subvolume, I need to specify rootflags=subvol=@ as well.
arch-bootloader
Configuring the second-stage bootloader has always been the hardest part of installing Linux for me.

4. Reboot

If everything was set up correctly, we should be able to reboot into our freshly-installed system:

  • Leave the chroot environment by exiting the current shell
  • (Optionally) unmount partitions with umount -R /mnt
  • reboot
  • Choose the correct option in our boot loader of choice
refind
rEFInd has already detected my existing Ubuntu install and has correctly set up a corresponding chain-load entry.

You’ll then be prompted for your disk encryption password before fully booting:

arch-boot-encrypted

5. Post-installation

All done! I’ll leave any further post-installation tutorials to future posts.