Installing ubuntu 20.04 with root on encrypted ZFS mirror and UEFI boot

Installing ubuntu 20.04 with root on encrypted ZFS mirror and UEFI boot

My home machine has been running a pair of disks in a soft raid1 for 7 years now. And then the other day one disk in the mirror finally started to crumble. There was a reason to reinstall the system from scratch and start using the encryption that 7 years ago was not involved. While googling the status of LUKS over mdadm configuration, I came across an article comparing zfs vs mdadm/ext4 performance. I then found another article testing the performance of encrypted disks using LUKS and zfs. According to both articles the performance of zfs is quite good and I decided to give it a try.

I decided to write my own article since I use UEFI for booting (the previous articles used legacy booting) and it has been 3 years since the last one and I thought that a well tested up-to-date tutorial could be useful for the community.

My installation was mostly guided by these articles:
Ubuntu 20.04 Root on ZFS
Installing UEFI ZFS Root on Ubuntu 20.04 with Native Encryption

I will describe the installation in a virtualbox virtual machine. The installation on a real machine is no different.

So I created a virtual machine with a couple of 20gb disks and 8gb of memory. I booted with ubuntu-20.04.1-desktop-amd64.iso, clicked on try ubuntu and started the terminal. In the terminal, I went straight to root, since all the commands used require root privileges. The first thing I did was to define some variables:

export DISK1=/dev/disk/by-id/ata-VBOX_HARDDISK_VBad5107ca-df268eef
export DISK2=dev/disk/by-id/ata-VBOX_HARDDISK_VBaf134a71-943e2d11
export HOSTNAME=ubuntu-zfs-vm
export USERNAME=toor

Now you can partition the disks:

sgdisk --zap-all $DISK1
sgdisk --zap-all $DISK2
sgdisk -n1:1M:+256M -t1:EF00 -c1:EFI $DISK1
sgdisk -n1:1M:+256M -t1:EF00 -c1:EFI $DISK2
sgdisk -n2:0:+1024M -t2:be00 -c2:Boot $DISK1
sgdisk -n2:0:+1024M -t2:be00 -c2:Boot $DISK2
sgdisk -n3:0:0 -t3:bf00 -c3:Ubuntu $DISK1
sgdisk -n3:0:0 -t3:bf00 -c3:Ubuntu $DISK2

I will use UEFI to boot, so I need to create a disk with type EF00 formatted to vfat:

mkfs.msdos -F 32 -n EFI ${DISK1}-part1
mkfs.msdos -F 32 -n EFI ${DISK2}-part1

Now it’s time to create zfs. I will use grub, which supports booting from zfs, although not with all options. So creating a boot partition requires explicitly specifying only what grub understands:

zpool create -f -o cachefile=/etc/zfs/zpool.cache -o ashift=12 \
-o autotrim=on -d -o feature@async_destroy=enabled
-o feature@bookmarks=enabled -o feature@embedded_data=enabled
-o feature@empty_bpobj=enabled -o feature@enabled_txg=enabled \
-o feature@extensible_dataset=enabled
-o feature@filesystem_limits=enabled -o feature@hole_birth=enabled
-o feature@large_blocks=enabled -o feature@lz4_compress=enabled
-o feature@spacemap_histogram=enabled -O acltype=posixacl -O canmount=off \
-O compression=lz4 -O devices=off -O normalization=formD -O relatime=on
-O xattr=sa -O mountpoint=/boot -R /mnt \
bpool mirror ${DISK1}-part2 ${DISK2}-part2

The boot partition is created and we can now create the root partition (since we are using encryption we will need to type in the password):

zpool create -f -o ashift=12 -o autotrim=on -O encryption=aes-256-gcm \
-O keyylocation=prompt -O keyformat=passphrase -O acltype=posixacl \
-O canmount=off -O compression=lz4 -O dnodesize=auto
-O normalization=formD -O relatime=on -O xattr=sa -O mountpoint=/
-R /mnt rpool mirror ${DISK1}-part3 ${DISK2}-part3/
You can create datasets. I decided to keep it to a minimum:
zfs create -o canmount=off -o mountpoint=none rpool/ROOT
zfs create -o canmount=off -o mountpoint=none bpool/BOOT
UUID=$(dd if=/dev/urandom bs=1 count=100 2>/dev/null \
|tr -dc 'a-z0-9' | cut -c-6)
zfs create -o mountpoint=/ -o com.ubuntu.zsys:bootfs=yes \
-o com.ubuntu.zsys:last-used=$(date +%s) \
rpool/ROOT/ubuntu_$UUID
zfs create -o mountpoint=/boot bpool/BOOT/ubuntu_$UUID
zfs create -o canmount=off -o mountpoint=/ rpool/USERDATA
zfs create -o com.ubuntu.zsys:bootfs-datasets=rpool/ROOT/ubuntu_$UUID \
-o canmount=on -o mountpoint=/home/$USERNAME \
rpool/USERDATA/${USERNAME}_$UUID

To install the system, let’s use debootstrap:

apt-get install -y debootstrap
debootstrap focal /mnt

Copy the missing components to the new filesystem:

echo $HOSTNAME >/mnt/etc/hostname
sed '/cdrom/d' /etc/apt/sources.list > /mnt/etc/apt/sources.list
sed "s/ubuntu/$HOSTNAME/" /etc/hosts > /mnt/etc/hosts
cp /etc/netplan/*.yaml /mnt/etc/netplan/

And mount the pseudofs you need to continue the installation:

mount --make-private --rbind /dev /mnt/dev
mount --make-private --rbind /proc /mnt/proc
mount --make-private --rbind /sys /mnt/sys

Enter the chroot environment:

chroot /mnt /usr/bin/env DISK1=$DISK1 DISK2=$DISK2 USERNAME=$USERNAME \
    /bin/bash -login

Update the indexes of the binary packages and set the locale:

apt-get update
locale-gen --purge "en_US.UTF-8"
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US
dpkg-reconfigure --frontend noninteractive locales

Set the desired time zone:

dpkg-reconfigure tzdata

Mount the EFI partition. Usually they mount it under /boot/efi, but in our case we have 2 partitions and there is a problem with the order in which the disks are mounted. I decided to mount the disk in a different hierarchy and use a symlink:

mkdir /run/efi1
mount $DISK1-part1 /run/efi1
ln -s /run/efi1 /boot/efi
echo /dev/disk/by-uuid/$(blkid -s UUID -o value \
    ${DISK1}-part1) /run/efi1 vfat defaults 0 0 >> /etc/fstab
echo /dev/disk/by-uuid/$(blkid -s UUID -o value \
    ${DISK2}-part1) /run/efi2 vfat defaults 0 0 >> /etc/fstab

Install all other needed packages:

apt-get install -y grub-efi-amd64 grub-efi-amd64-signed linux-image-generic \
    shim-signed zfs-initramfs zsys ubuntu-minimal network-manager

Because of the regression, we have to add the kernel parameter init_on_alloc=0:

sed -ie 's/\(GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\)/\1 init_on_alloc=0/' \
    /etc/default/grub

I prefer to have a small swap:

zfs create -V 4G -b $(getconf PAGESIZE) -o compression=off \
    -o logbias=throughput -o sync=always -o primarycache=metadata \
    -o secondarycache=none rpool/swap
mkswap -f /dev/zvol/rpool/swap
echo "/dev/zvol/rpool/swap none swap defaults 0 0". >> /etc/fstab
echo RESUME=none > /etc/initramfs-tools/conf.d/resume

Add a user:

adduser $USERNAME
find /etc/skel/ -type f|xargs cp -t /home/$USERNAME
chown -R $USERNAME:$USERNAME /home/$USERNAME
usermod -a -G adm,cdrom,dip,plugdev,sudo $USERNAME

Update inird and grub and get out of the chroot environment:

update-initramfs -c -k all
update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    -bootloader-id=ubuntu --recheck --no-floppy
exit

Unmount what was mounted in the chroot environment and reboot:

mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}
zpool export -a
reboot

Since everything happens in virtualbox I have to mention that with UEFI enabled the virtualbox refuses to boot from the optical drive. So at this point I remove the disk from the virtual drive and enable UEFI boot.

Unless something unforeseen happens you’ll see a grub menu. But don’t rush to press enter! Instead boot into recovery mode, because when importing the zfs root pool an error will occur, caused by the fact that the last time the pool was used on a machine with a different name. The fix for this is simple:

zpool import -f rpool
exit

After that you will be asked for your password to access the disk and your boot will continue up to the point where you will be asked to use the emergency console (because we are in recovery mode) or press ctrl-d to boot normally. Press ctrl-d. After a few seconds you will be able to login using the previously created user. This is not the end of our misadventures. Take a look at the /boot directory and you will see that it is empty. The boot pool has not been imported either. Let’s fix this:

zpool import bpool/
The final touch is to mark both EFI partitions as needing to be updated when grub changes:
dpkg-reconfigure grub-efi-amd64

Now the installation is completely finished and you can reboot and use the default grub menu item. You will see a black screen in the kernel options because the default is quiet. The disk access password will have to be entered blindly after a few seconds of booting. You can remove quiet from the parameters or put the plymouth package in.

All commands above can be downloaded in a single script which needs the variables DISK1, DISK2, HOSTNAME and USERNAME to work.

If you are looking for: the best courier service in Moscow

© All Right Reserved 2019-2020 futuredesktop.org