Alpine Linux on encrypted ZFS with rEFInd for UEFI

This article will cover how to install Alpine Linux with an encrypted ZFS root dataset, using rEFInd as a UEFI bootloader. I will only show setting up a single storage device, but you can use the OpenZFS Docs as a reference to see how you can easily extend my instructions for two mirrored devices. I will also not cover a setup with secureboot enabled, honestly I have not done it yet myself.

This article is based on the following two articles, as well as my own research and testing:

I personally use this setup for my everyday laptop, the encryption protects my data in case of theft and ZFS will let me know if there is ever any corruption (though you need at least two disks to prevent it). The ability to create very quick and low-overhead snapshots is also very useful.

Preparing a bootable USB

For setting up ZFS, you need the “Extended” Alpine Linux image from the Alpine Linux downloads. I recommend also at least verifying its SHA256 sum for data integrity since it’s quite a big file at >900MB.

Determine the name of your USB stick with lsblk or fdisk -l, it should be something like sdX. You need to be absolutely sure that this is your USB stick because we will be overwriting the contents of this device with the downloaded ISO file. To write the ISO to the device, use the following command, replacing sdX with the name of your device and alpine-extended.iso with the path to the downloaded ISO file:

dd if=alpine-extended.iso of=/dev/sdX bs=1M conv=fsync

The conv=fsync option ensures that the data is fully written to the drive. Now eject the device:

eject /dev/sdX

Booting from USB

Now we can restart the computer and boot the USB stick. Make sure that you are booting in UEFI mode, there may be a setting in your BIOS/UEFI menu to boot with “UEFI First” or “UEFI Only”. Also make sure that secureboot is disabled. Once you booted into the USB stick, you can log in with the username “root” with no password.

If you are not sure whether you booted in UEFI mode, you can install efibootmgr and run it (you can remove it afterwards):

apk add efibootmgr
efibootmgr

Unless it says “EFI variables are not supported on this system” or another error, we can go on.

Preparing the installation environment

First, run setup-alpine and go through the installation as normal until it asks which disk to install to, then answer “none” to the remaining questions until it’s done.

Now we run setup-apkrepos again in order to get access to the edge/testing repository, which is where we get rEFInd. Press ‘c’ to enable the community repository, then press ’e’ to edit the file “/etc/apk/repositories” manually. Below the URLs for the main and community repositories, we can add a line like this for the testing repository:

https://YOUR_MIRROR/alpine/X.XX/main
https://YOUR_MIRROR/alpine/X.XX/community
@testing https://YOUR_MIRROR/alpine/edge/testing

You may notice that it’s “edge/testing”, not “X.XX/testing”. Besides the stable versioned Alpine releases, there are “edge” repositories which contain the newest versions of all packages as a rolling release. The “testing” repository only exists on “edge”, so essentially we are mixing packages from a stable release with “edge” here, which is not recommended. I personally only had a problem once (which was not serious), so this is acceptable to me for a single package. I prefix the line with “@testing”, which forces me to specify “packagename@testing” if I want to install a package from the testing repository.

Now we install the packages that we need in order to set up the system:

apk update
apk add dosfstools sfdisk zfs util-linux refind@testing

We also need eudev to get predictable disk names:

apk add eudev 
setup-devd udev

You can remove this after rebooting with setup-devd mdev && apk del eudev.

Partitioning and setting up ZFS

First, identify the disk you want to install to and set these variables for the upcoming steps:

find /dev/disk/by-id/

# Replace NAME with the disk you want to install to
DISK="/dev/disk/by-id/NAME"
MNT=$(mktemp -d)

Now create the EFI System Partition (ESP), which will contain “/boot”, and the partition that will be used for ZFS. Feel free to adjust the size of the partitions to your liking:

sfdisk --wipe always --wipe-partitions always "${DISK}" <<EOF
label: gpt
size=4GiB, name=EFI, type=uefi
name=rpool
EOF

# Run partprobe to inform the OS that the partitions changed
partprobe "${DISK}"

Now we can create a ZFS zpool on the second partition, I will call it “rpool”. You can find documentation for zpool create in the zpool-create(8) manpage. The -o options can be read about in zpoolprops(7) and the -O options in zfsprops(7). Feel free to adjust the options as needed, here I configure the encryption with a passphrase prompt but you can also use a hardware key or key file depending on what fits your use case.

# Load the ZFS kernel module
modprobe zfs

zpool create \
	-o ashift=12 \
	-o autotrim=on \
	-O acltype=posix \
	-O canmount=off \
	-O dnodesize=auto \
	-O encryption=on \
	-O keyformat=passphrase \
	-O keylocation=prompt \
	-O normalization=formD \
	-O relatime=on \
	-O xattr=sa \
	-O mountpoint=none \
	-R "${MNT}" \
	rpool \
	"${DISK}"-part2

You can verify that the pool was created with zpool status. Finally, we create the ZFS datasets where we will mount the root and /home directory. You can also just use a single root dataset for everything or create even more datasets for other mountpoints depending on your needs.

zfs create -o canmount=noauto -o mountpoint=legacy rpool/root
zfs create -o mountpoint=legacy rpool/home

You can verify the datasets with zfs list. Finally, mount the datasets:

mount -o X-mount.mkdir -t zfs rpool/root "${MNT}"
mount -o X-mount.mkdir -t zfs rpool/home "${MNT}"/home

Now we can create a FAT32 filesystem on the first partition and mount it to where our /boot will be:

mkfs.fat -F32 -n EFI "${DISK}"-part1

mount -t vfat \
	-o fmask=0077,dmask=0077,iocharset=iso8859-1,X-mount.mkdir \
	"${DISK}"-part1 "${MNT}"/boot

Installing Alpine and setting up rEFInd

To install Alpine to the partitions we mounted, we use the setup-disk command. We use BOOTLOADER=none since we will be manually installing the bootloader:

BOOTLOADER=none setup-disk -v "${MNT}"

Now we can install rEFInd, we can use the --root option to install it to our mountpoint:

refind-install --root "${MNT}"

To make Alpine bootable by rEFInd, we need to create the file /boot/refind_linux.conf with the kernel options that are needed to boot. Here is an example for a quiet or more verbose boot, feel free to adjust the modules or add other options as needed:

tee -a "${MNT}"/boot/refind_linux.conf <<EOF
"Quiet" "modules=sd-mod,usb-storage,zfs root=rpool/root rootfstype=zfs quiet"
"Verbose" "modules=sd-mod,usb-storage,zfs root=rpool/root rootfstype=zfs"
EOF

Finishing up

Before we can reboot, we have to unmount the partitions/datasets:

umount -Rl "${MNT}"

Now we can create an initial snapshot that we can revert to later if needed:

zfs snapshot -r rpool@initial-installation

You can verify that it was created with zfs list -t snapshot. Finally, export the ZFS storage pool and reboot:

zpool export -a
reboot

The system should start up with rEFInd, since refind-install sets it as the default boot entry. ZFS will prompt you for your passphrase if you configured it that way and you should have a working Alpine installation. Enjoy!

If you have any feedback or suggestions, please email me at unicorn@regrow.earth.