Installing Arch Linux on encrypted ZFS and ZFSBootMenu

This is a straightforward tutorial on how to install Arch Linux on a ZFS Encrypted zpool, for those who are familiar with the Arch Linux installation process. The focus here is to provide knowledge on the ZFS part of the equation and not to replace the procedures that are already extensively documented at the Arch Wiki.

Some installation steps are redundant and there is plenty of material over the internet including this blog and the Arch Wiki.

## Motivations

I started to consider that I should use a more flexible filesystem in the sense I could easily revert to a previous state after I’ve had some really bad situations with nvidia, which broke my setup last year:

These incidents that I’ve documented on the Gaming On Linux community plus three times I had to use a live USB to recover my Arch install because of dkms error when building the nvidia module made me think I should implement some mechanism where from the bootloader I could revert to a previous working state.

Also, since it is a laptop, data-at-rest encryption is a must.

## The Contenders

While I had some previous job experiences with zfs managing Solaris and FreeBSD, I wanted to prioritize the use of any in-tree technologies. These were my options and the rationale why I didn’t pick them:

  • luks + lvm + xfs / ext4 - While a really reliable solution which I have deployed countless VMs and let the snapshot part to be managed by some hypervisor, lvm2 snapshots are cluttered and you have to configure sizes for them and they can be easily damaged when they reach max size. The fact that lvm does not behave well on chained snapshots does not help much and, having to rely on another technology for compression and deduplication (vdo) just make things overly complicated.
    • Yes, I’m aware that you can configure snapshot_autoextend_threshold and snapshot_autoextend_percent in /etc/lvm/lvm.conf, but that resize might not be online and depending on the size of an Arch Linux update, it is prone to break. Sometimes you can have 2GB worth of updates and lvm will not resize quickly enough
  • luks + btrfs - I’ve had some good performance and compression with zstd:7 on my secondary nvme mounted at /data and auto unlocked through a keyfile in my main disk. That directory only had data I was willing to lose if something goes wrong(Games, Music, Videos) and oh boy, one day I’ve reached one of those unrecoverable errors on btrfs that not even a scrub would fix it.
  • luks + bcachefs - Too much drama plus the fact that a filesystem that aims for data to be persistent to use the word cache in it’s name is just, weird…

Another limiting factor is that grub2 still does not support the majority of of ciphers from luks2, and grub is the only bootloader option that is able to index and boot btrfs snapshots. Downgrading to luks1 is not an option.

How it feels managing lvm snapshots: lvm user managing snapshot sizes on a 5+ chained snapshot configuration on thin privisioned LVM

## ZFS Drawbacks

ZFS is an out-of-tree dkms module, and while people use it due to it's features and because of that, you should not ask support on Arch Linux forums

Most of the disadvantages of using zfs on Linux are higlighted on this Arch Wiki article, but, let’s reiterate them:

  • ZFS does not encrypt metadata, meaning some basic structures of your pool like dataset, snapshot names, snapshot hierarchy, and deduplication tables (although deduplicated data is encrypted) might be visible with the adequate tools.
  • Pool creation requires knowledge of disk geometry. With nvmes, some of the pain point for tuning are gone, but not all of them.
  • Swap inside a zvol is not possible and it is an old and well-known issue and you should avoid using it as well as using swapfiles inside ZFS.
  • ZFS has some caveats due to it’s implementation of aes for cryptography and had some performance issues in the past that are now fixed. They had to implement their own mechanisms since they can’t touch kernel algorithms because the module is not licensed as GPL.
  • out-of-tree kernel module. Pretty obvious but, if you don’t want any surprises like your module not being added to the initramfs, pay attention during kernel install procedures or just use linux-lts for better compatibility.
  • If you are a laptop user, disable hibernate(suspend to disk) entirely. Waking your laptop, importing your zfs pool and after that bringing data that is hibernate from swap back to your disk will likely break your zfs pool.

Now that you are aware that risks exist, let’s follow up with the setup process :)

## Requisites

Either you create a custom Archiso with the zfs modules or you use the pre-built archzfs-iso. This is needed so you have support on zfs without having to compile stuff in live environment.

You should also know how to connect to the internet, set your keyboard layout and all these tasks to proceed with an Arch Linux install using the Arch Live iso.

## Setup

Again, this is not a detailed procedure on Arch Linux installation itself and you should also use the Arch Wiki Installation Guide as a suplementary material to this guide. Our focus here is the ZFS deployment.

Create the following three partitions on your disk:

  • First: 512MB, type ef00 (ESP)
  • Second: 4GB, type 8309(Linux Luks). This will hold our automatically encrypted swap partition that will re-encrypted automatically each boot.
  • Third: Rest of the disk size, type bf00 (Solaris root) that will be used as our device for our zpool.

Generate your /etc/hostid with the following command

zgenhostid

Store your pool passphrase in a file with the following commands:

echo '<passphrase>' > /etc/zfs/zroot.key
chmod 000 /etc/zfs/zroot.key

Now, create your zpool while also replacing your pool name and device partition:

zpool create -f -o ashift=12 \
 -O compression=lz4 \
 -O acltype=posixacl \
 -O xattr=sa \
 -O relatime=on \
 -O encryption=aes-256-gcm \
 -O keylocation=file:///etc/zfs/zroot.key \
 -O keyformat=passphrase \
 -o autotrim=on \
 -m none zroot /dev/nvme0n1p3

zroot is an arbitrary name, please use one at your convenience.

You might want to change ashift= depending on your hard drive specification. While some modern SSDs perform better with ashift=13 because they work with 8KiB block size, most nvmes report 4KiB sector size to the operating system so, ashift=12 is a good value to “play safe”. Encryption algorithm aes-256-gcm is great security wise and has good performance in some of my testings with kdiskmark and fio.

Create the datasets with the mount points you desire. I’ll go simple here with my root filesystem and /home:

zfs create -o mountpoint=none zroot/ROOT
zfs create -o mountpoint=/ -o canmount=noauto zroot/ROOT/arch
zfs create -o mountpoint=/home zroot/home
/tmp is handled by systemd tpmfs and should not be mounted separately. /bin and /sbin are part of the /usr unification so, be extra careful when having this top level dir as a dataset

We’re isolating our root mountpoints under zroot/ROOT so in the future if you want to copy your installation or dual boot with other Linux, it’s easier. Some zfs properties will be shared amongst all root datasets so, better put them under the same umbrella. We’re also setting canmount=noauto on our Arch dataset because root device mounting will be handled by the bootloader to allow us to have multiple root devices while not having all of them mounted at the seme time.

Now, export the pool and reimport it under /mnt:

zpool export zroot
zpool import -N -R /mnt zroot
zfs load-key -L prompt zroot
zfs mount zroot/ROOT/arch
zfs mount zroot/home

Format your ESP:

mkfs.vfat -F 32 -n EFI /dev/nvme0n1p1
mkdir /mnt/efi
mount /dev/nvme0n1p1 /mnt/efi

Install arch Linux using pacstrap. Please consult the Arch Wiki Installation Guide for more details

pacstrap /mnt base linux-lts linux-firmware linux-lts-headers curl vim

Please note that during the pacstrap process you can install any package you want, and even install microcode related packages, GUIs, web browsers but on this tutorial, we’ll focus on the bare minimum for a machine to boot zfs.

Install the zfs-dkms package:

pacstrap /mnt zfs-dkms

Copy the relevant files to your chroot environment:

cp /etc/hostid /mnt/etc
cp /etc/resolv.conf /mnt/etc
mkdir /mnt/etc/zfs   # plese ignore if you get prompted it already exists
cp /etc/zfs/zroot.key /mnt/etc/zfs
cp /etc/pacman.conf /mnt/etc/pacman.conf

Generate the /etc/fstab file:

genfstab /mnt > /mnt/etc/fstab

Edit /mnt/etc/fstab and remove all lines except the one containing /efi.

Chroot into arch:

arch-chroot /mnt

Execute any additional tasks like (but not limited to): Set your timezone, locales, language and keyboard, network configuration, hostname, root password, create your user and set a password, etc.

Prepare your initramfs. Edit /etc/mkinitcpio.conf and add your zroot.key to the FILES= section, and add zfs in your HOOKS section before the filesystems hook:

FILES=(/etc/zfs/zroot.key)

and

HOOKS=(base udev autodetect microcode modconf keyboard keymap block zfs filesystems)

Force generate a new initramfs:

mkinitcpio -P

Set a cachefile for your zpool and bootfs for your zroot/ROOT/arch dataset

zpool set cachefile=/etc/zfs/zpool.cache zroot
zpool set bootfs=zroot/ROOT/arch zroot

In the future, if you dual boot other Linux distribution (let’s say zroot/ROOT/debian for example), you will need to set this property to this dataset as well.

Now, set the kernel parameters to zroot/ROOT, and those will be inherited by any child dataset (like zroot/ROOT/arch):

zfs set org.zfsbootmenu:commandline="noresume init_on_alloc=0 rw spl.spl_hostid=$(hostid)" zroot/ROOT

Enable all services that are needed by zfs:

systemctl enable zfs-import-cache zfs-import.target zfs-mount zfs-zed zfs.target

## Bootloader

I’m using a highly specialized EFI Loader called ZFSBootMenu (ZBM), which is a neat piece of software that manages multiple aspects of ZFS boot environments and supports ZFS native encryption. To download it and enable it as a bootable EFI binary:

mkdir -p /efi/EFI/zbm

curl https://get.zfsbootmenu.org/latest.EFI --output /efi/EFI/zbm/zfsbootmenu.EFI

efibootmgr --disk /dev/nvme0n1 --part 1 --create --label "ZFSBootMenu" --loader '\EFI\zbm\zfsbootmenu.EFI' --unicode "spl_hostid=$(hostid) zbm.timeout=3 zbm.prefer=zroot zbm.import_policy=hostid" --verbose

ZBM is also provided by AUR but it is a dead simple EFI executable and I prefer to manage it outside the package manager.

Now, you just have to reboot and enjoy:

exit
umount /mnt/efi
zpool export zroot
reboot

## Self-encrypted Swap

Because of the nature of this operation that will quickly wipe and format your swap partition every boot, you should NOT use naming orders like /dev/nvme, by-path or by-id naming, or UUID since these are refreshed because of each re-encryption of your swap.

There is a really neat procedure to auto format and encrypt your swap that will rotate your key every boot and it is described here.

First, we need to find out what is the partuuid of /dev/nvme0n1p2:

ls -l  /dev/disk/by-partuuid/ | grep /nvme1n1p2

After that, add this line at the end of your /etc/crypttab and put the value with the PARTUUID of your disk:

swap         PARTUUID=6eac8d50-xxxx-yyyy-zzzz-83a1877f9c95                  /dev/urandom            swap,cipher=aes-xts-plain64,size=512,sector-size=4096

This will automatically create a swap device at /dev/mapper/swap that needs to be added to your /etc/fstab. Add the following line at the end of your fstab:

/dev/mapper/swap  none swap discard 0 0

Done. Now you have a secure swap that not even you know the secrets to unlock it, and it will be rotated every boot.

## Auto Snapshots

After rebooting and login in, you can check all your zfs datasets with:

zfs list

To automate the snapshot process daily, you’ll have to edit the zfs-auto-snapshot-daily.service as root:

systemctl edit zfs-auto-snapshot-daily.service

Create an override entry below the warning that states ### Anything between here and the comment below will become the contents of the drop-in file:

[Service]
ExecStart=
ExecStart=/bin/zfs-auto-snapshot --skip-scrub --prefix=znap --label=daily --keep=7 //

Explanation: The first empty ExecStart= will clear that line and it is pretty much needed on systemd override files, while the second one is the new line with the amount of days yout want to keep worth of snapshots. Please change --keep-7 to a value that makes sense to you. The default unit provided value is --keep=31 which is huge

After some days, the output of zfs list -t snapshot will show you something like this:

zroot/ROOT@znap_2025-08-27-2300_daily               0B      -   192K  -
zroot/ROOT@znap_2025-08-28-2317_daily               0B      -   192K  -
zroot/ROOT@znap_2025-08-29-2300_daily               0B      -   192K  -
zroot/ROOT@znap_2025-08-30-2300_daily               0B      -   192K  -
zroot/ROOT@znap_2025-08-31-2300_daily               0B      -   192K  -
zroot/ROOT@znap_2025-09-01-2300_daily               0B      -   192K  -
zroot/ROOT@znap_2025-09-02-2300_daily               0B      -   192K  -
zroot/ROOT/archlinux@znap_2025-08-27-2300_daily   206M      -  27.5G  -
zroot/ROOT/archlinux@znap_2025-08-28-2317_daily  50.6M      -  27.4G  -
zroot/ROOT/archlinux@znap_2025-08-29-2300_daily  92.6M      -  27.5G  -
zroot/ROOT/archlinux@znap_2025-08-30-2300_daily   168M      -  27.8G  -
zroot/ROOT/archlinux@znap_2025-08-31-2300_daily   120M      -  26.1G  -
zroot/ROOT/archlinux@znap_2025-09-01-2300_daily   125M      -  26.4G  -
zroot/ROOT/archlinux@znap_2025-09-02-2300_daily  15.4M      -  26.7G  -
zroot/home@znap_2025-08-27-2300_daily             351M      -   979G  -
zroot/home@znap_2025-08-28-2317_daily             119M      -   979G  -
zroot/home@znap_2025-08-29-2300_daily             609M      -   979G  -
zroot/home@znap_2025-08-30-2300_daily             463M      -   921G  -
zroot/home@znap_2025-08-31-2300_daily            1.02G      -   922G  -
zroot/home@znap_2025-09-01-2300_daily            3.06G      -   951G  -
zroot/home@znap_2025-09-02-2300_daily            45.0M      -   892G  -

All snapshots will have the @znap_DATE_daily sufix, indicating they were auto generated.

If you have a dataset you don’t want to automatically snapshot, add the com.sun:auto-snapshot=false property to that dataset.

Example if I wanted to disable snapshots in my /home mounted dataset:

zfs set com.sun:auto-snapshot=false zroot/home

## Scrub

A ZFS scrub is a process that verifies the integrity of all data within a ZFS storage pool by re-reading all blocks and comparing them against their stored checksums. Scrubbing regularly your filesystem is a nice way to check data integrity and keep things healthy.

On Arch linux, the package systemd-zpool-scrub can help you with that maintenance task, and you can enable automatic scrub on your zpool bu hist adding an @DATASET between the timer unit name and the .timer sufix:

systemctl status zpool-scrub@zpool.timer

This will enable the timer that trigger a weekly scrub on the zpool pool.

For laptop users, worry not because this procedure will only run if you are plugged to your power cord.

## Final Considerations

This is a really narrow-down procedure on how to have zfs as your root filesystem on Arch.

Consider also:

  • Deploying Secure Boot with sbctl and sign /efi/EFI/zbm/zfsbootmenu.EFI with your own keys
  • Take regular backups on another machine with zfs with zfs send

Extra Resources:

  • Practical ZFS is a nice forum full of folks that are really knowledgeable on ZFS and willing to help.
  • r/zfs subreddit has some interesting discussions regarding unique setups. It is still reddit so, you may find the usual internet behavior there as well.

On my next topic I will likely share some extra configurations that I’ve made to increase performance of my zfs pool on nvmes.

Stay safe!