Manual Ubuntu installations

Encrypted ZFS Root on Ubuntu Server 20.04 (with USB Unlock)

It’s all nice and dandy to setup unencrypted ZFS on server or setting it up with boot encryption. However, what if we want to use USB to unlock the encrypted drive? And no, it’s not as crazy as it seems. Scenarios are actually plentiful.

One scenario is when you have your servers encrypted (as realistically everybody should) but you don’t necessarily want (or can) enter the password. If you can plug in USB with a key and make ZFS use that key, you suddenly have password-less boot without connecting to the server. After boot is done, you can unplug the USB and store it somewhere safe. And this can be done by literally anybody you trust - it doesn’t have to be you.

My favorite scenario is using it with self-erasing USB drive. You place the encryption key on the small drive and it will be there for every boot. You can use your server as you normally would. However, if power is lost, your key will disappear and content of server will not be accessible anymore. When would such crazy scenario happen you ask? Well, if anybody is stealing your server they have to unplug it first. And yes, your server is gone but at least your data is not.

I admit I never had that scenario happen to me - fortunately all my servers are still accounted for. But I did RMA disk drives. And worrying about erasing the data when you cannot access it anymore is a bit too late.

Whatever might be your case, let me guide you through setting up natively-encrypted ZFS with a key on the USB drive.

Once you enter the shell of the installation media, the very first step is setting up a few variables - location of disk and USB drive, followed by pool and host name. This way we can use them going forward and avoid accidental mistakes. Make sure to replace these values with the ones appropriate for your system.

DISK=/dev/disk/by-id/^^ata-xxx^^
USB=/dev/disk/by-id/^^usb-xxx^^
POOL=^^Ubuntu^^
HOST=^^server^^

Next let’s sort out question of the encryption key. Assumption is that the key will be on the first partition of the FAT formatted USB drive and we’ll mount it at /tmpusb. While you could create the key material directly, I personally prefer the passphrase as it makes life easier in the case of recovery. If you already have the passphrase on the drive just skip the last command as it will overwrite it.

mkdir /tmpusb
mount -t vfat -o rw "$USB-part1" /tmpusb
echo -n "^^password^^" > /tmpusb/boot.pwd

General idea of my disk setup is to maximize amount of space available for pool with the minimum of supporting partitions. If you are installing on SSD blkdiscard will trim all the data. You can safely ignore any errors on disks that don’t support it.

blkdiscard $DISK 2>/dev/null

sgdisk --zap-all                        $DISK

sgdisk -n1:1M:+127M -t1:EF00 -c1:EFI    $DISK
sgdisk -n2:0:+512M  -t2:8300 -c2:Boot   $DISK
sgdisk -n3:0:0      -t3:8309 -c3:Ubuntu $DISK

sgdisk --print                          $DISK

To kick off the fun of the installation we need debootstrap and zfsutils-linux package.

apt update
apt install --yes debootstrap zfsutils-linux

Now we’re ready to create system ZFS pool.

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O encryption=aes-256-gcm -O keyformat=passphrase -O keylocation=file:///tmpusb/boot.pwd \
    -O canmount=off -O mountpoint=none -R /mnt/install $POOL $DISK-part3
zfs create -o canmount=noauto -o mountpoint=/ $POOL/System
zfs mount $POOL/System

Assuming UEFI boot, two additional partitions are needed - one for EFI and one for booting. I don’t have ZFS pool for boot partition but a plain old ext4 as I find potential fixup works better that way.

yes | mkfs.ext4 $DISK-part2
mkdir /mnt/install/boot
mount $DISK-part2 /mnt/install/boot/

mkfs.msdos -F 32 -n EFI $DISK-part1
mkdir /mnt/install/boot/efi
mount $DISK-part1 /mnt/install/boot/efi

Bootstrapping Ubuntu on the newly created pool is next. This will take a while.

debootstrap focal /mnt/install/

zfs set devices=off $POOL

Our newly copied system is lacking a few files and we should make sure they exist before proceeding.

echo $HOST > /mnt/install/etc/hostname
sed '/cdrom/d' /etc/apt/sources.list > /mnt/install/etc/apt/sources.list
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

Finally we’re ready to “chroot” into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env DISK=$DISK USB=$USB POOL=$POOL bash --login

Let’s not forget to setup locale and time zone.

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

dpkg-reconfigure tzdata

To mount EFI and boot partitions, we need to do some fstab setup too:

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
cat /etc/fstab

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

Followed by the boot environment packages.

apt install --yes zfs-initramfs plymouth grub-efi-amd64-signed shim-signed

Now it’s time to setup boot scripts to ensure USB drive is mounted before ZFS needs it. I found that initramfs’ init-premount directory is the ideal spot.

cat << EOF > /usr/share/initramfs-tools/scripts/init-premount/tmpusb
#!/bin/sh -e

PREREQ="udev"
prereqs() {
    echo "\$PREREQ"
}

case \$1 in
    prereqs)
        prereqs
        exit 0
    ;;
esac

USB="$USB"
POOL="$POOL"

echo "Waiting for \$USB"
for I in \`seq 1 20\`; do
    if [ -e "\$USB" ]; then break; fi
    echo -n .
    sleep 1
done
echo

sleep 2

if [ -e "\$USB" ]; then
    mkdir /tmpusb
    mount -t vfat -o ro "\$USB-part1" /tmpusb
    if [ \$? -eq 0 ]; then
        exit 0
    else
        echo "Error mounting \$USB-part1" >&2
    fi
else
    echo "Cannot find \$USB" >&2
fi
exit 1
EOF
# chmod 755 /usr/share/initramfs-tools/scripts/init-premount/tmpusb

# cat << EOF > /usr/share/initramfs-tools/scripts/init-bottom/tmpusb
#!/bin/sh -e

PREREQ="udev"
prereqs() {
    echo "\$PREREQ"
}

case \$1 in
    prereqs)
        prereqs
        exit 0
    ;;
esac

if [ -e "/tmpusb" ]; then
    umount /tmpusb
    rmdir /tmpusb
fi
EOF

chmod 755 /usr/share/initramfs-tools/scripts/init-bottom/tmpusb

The first script will wait for USB drive if needed and mount it at /tmpusb for ZFS to find. Second script is there just for a bit of cleanup.

If USB drive is not mounted, this will cause boot to fail. If we want ZFS to ask for the passphrase instead (despite having file as the keylocation) a further customization is needed. But note these commands might need adjustment and they definitely need to be repeated each time ZFS package is updated. I might go into the details in some future post but suffice to say this is really not future-proof solution but it’s the minimum set of changes that I could make sed work with.

sed -i 's/load-key/load-key -L prompt/' /usr/share/initramfs-tools/scripts/zfs
sed -i '0,/load-key/ {s/-L prompt//}' /usr/share/initramfs-tools/scripts/zfs
sed -i '/KEYSTATUS=/i \\t\t\t$ZFS load-key "${ENCRYPTIONROOT}"' /usr/share/initramfs-tools/scripts/zfs
sed -i '/KEYSTATUS=/i \\t\t\tKEYLOCATION=prompt' /usr/share/initramfs-tools/scripts/zfs

In lieu of warning, suffice it to say these changes to zfs script are suitable only for this scenario and don’t really work for anything else.

Now we get grub started and update our boot environment. Due to Ubuntu 19.10 having some kernel version kerfuffle, we need to manually create initramfs image. This is also a good moment to check if our script is in.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -u -k $KERNEL

lsinitramfs /boot/initrd.img-$KERNEL | grep tmpusb

Grub update is what makes EFI tick.

update-grub 2>/dev/null
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy

Short package upgrade will not hurt.

apt dist-upgrade --yes

We can omit creation of the swap dataset but I personally find its good to have it just in case.

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

This is a good time to install other packages (e.g.,openssh-server) and do any setup you might need (e.g.,firewall). If nothing else, then setup root password so you have a way to log in (I personally prefer to create another user and leave root passwordless).

passwd

As installation is finally done, we can exit our chroot environment.

exit

And cleanup mount points.

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

After the reboot you should be able to enjoy your installation.

reboot

Installing UEFI ZFS Root on Ubuntu 21.10

Before reading further you should know that Ubuntu has a ZFS setup option since 19.10. You should use it instead of the manual installation procedure unless you need something special. In my case that special something is the native ZFS encryption and custom partitioning I find more suitable for laptop.

After booting into Ubuntu desktop installation we want to get a root prompt. All further commands are going to need root credentials anyhow.

sudo -i

The very first step should be setting up a few variables - disk, pool, host name, and user name. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system. I like to use upper-case for ZFS pool as that’s what will appear as password prompt. It just looks nicer and ZFS doesn’t care either way.

DISK=/dev/disk/by-id/^^ata_disk^^
POOL=^^Ubuntu^^
HOST=^^desktop^^
USER=^^user^^

General idea of my disk setup is to maximize amount of space available for pool with the minimum of supporting partitions. If you are not planning to have multiple kernels, decreasing boot partition size might be a good idea (512 MB is ok).

blkdiscard -f $DISK

sgdisk --zap-all                     $DISK

sgdisk -n1:1M:+63M -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+960M -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:0     -t3:BF00 -c3:Root $DISK

sgdisk --print                       $DISK

Finally we’re ready to create system ZFS pool. Note that you need to encrypt it at the moment it’s created.

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O encryption=aes-256-gcm -O keylocation=prompt -O keyformat=passphrase \
    -O canmount=off -O mountpoint=none $POOL $DISK-part3

On top of this encrypted pool we could create our root set (as I did in previous guides) or just use default dataset itself. I found that actually works better for me. In either case, our new root file system will end up at /mnt/install.

zfs set canmount=noauto $POOL
zfs set mountpoint=/ $POOL

mkdir /mnt/install
mount -t zfs -o zfsutil $POOL /mnt/install

Assuming UEFI boot, two additional partitions are needed. One for EFI and one for booting. Unlike what you get with the official guide, here I don’t have ZFS pool for boot partition but a plain old ext4. I find potential fixup works better that way and there is a better boot compatibility. If you are thinking about mirroring, making it bigger and ZFS might be a good idea. For a single disk, ext4 will do.

yes | mkfs.ext4 $DISK-part2
mkdir /mnt/install/boot
mount $DISK-part2 /mnt/install/boot/

mkfs.msdos -F 32 -n EFI $DISK-part1
mkdir /mnt/install/boot/efi
mount $DISK-part1 /mnt/install/boot/efi

To start the fun we need debootstrap package.

apt install --yes debootstrap

Bootstrapping Ubuntu on the newly created pool is next. This will take a while.

debootstrap $(basename `ls -d /cdrom/dists/*/ | grep -v stable | head -1`) /mnt/install/

We can use our live system to update a few files on our new installation.

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

If you are installing via WiFi, you might as well copy your wireless credentials. Don’t worry if this returns errors - that just means you are not using wireless.

mkdir -p /mnt/install/etc/NetworkManager/system-connections/
cp /etc/NetworkManager/system-connections/* /mnt/install/etc/NetworkManager/system-connections/

Finally we’re ready to “chroot” into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install \
    /usr/bin/env DISK=$DISK POOL=$POOL USER=$USER \
    bash --login

Let’s not forget to setup locale and time zone.

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

dpkg-reconfigure tzdata

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

Followed by boot environment packages.

apt install --yes zfs-initramfs grub-efi-amd64-signed shim-signed tasksel

To mount boot and EFI partition, we need to do some fstab setup.

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
cat /etc/fstab

Now we get grub started and update our boot environment.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -u -k $KERNEL

Grub update is what makes EFI tick.

update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy

Finally we install out GUI environment. I personally like ubuntu-desktop-minimal but you can opt for ubuntu-desktop. In any case, it’ll take a considerable amount of time.

tasksel install ubuntu-desktop-minimal

Short package upgrade will not hurt.

apt dist-upgrade --yes

We can omit creation of the swap dataset but I personally find a small one handy.

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

If one is so inclined, /home directory can get a separate dataset too but I usually skip it these days. I just proceed to create the user, assign a few extra groups to it, and make sure its home has correct owner.

adduser --disabled-password --gecos '' $USER
usermod -a -G adm,cdrom,dip,lpadmin,plugdev,sudo $USER
passwd $USER

As install is ready, we can exit our chroot environment.

exit

And cleanup our mount points.

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

After the reboot you should be able to enjoy your installation.

reboot

PS: There are versions of this guide using the native ZFS encryption for other Ubuntu versions: 22.04 and 20.04

PPS: For LUKS-based ZFS setup, check the following posts: 20.04, 19.10, 19.04, and 18.10.

Installing UEFI ZFS Root on Ubuntu 22.04

Before reading further you should know that Ubuntu has a ZFS setup option since 19.10. You should use it instead of the manual installation procedure unless you need something special. In my case that special something is the native ZFS encryption, UEFI boot, and custom partitioning I find more suitable for a single disk laptop.

After booting into Ubuntu desktop installation (via “Try Ubuntu” option) we want to open a terminal. Since all further commands are going to need root credentials, we can start with that.

sudo -i

The very first step should be setting up a few variables - disk, pool, host name, and user name. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system. I like to use upper-case for ZFS pool as that’s what will appear as password prompt. It just looks nicer and ZFS doesn’t care either way.

DISK=/dev/disk/by-id/^^ata_disk^^
POOL=^^Ubuntu^^
HOST=^^desktop^^
USER=^^user^^

General idea of my disk setup is to maximize amount of space available for pool with the minimum of supporting partitions. If you are not planning to have multiple kernels, decreasing boot partition size might be a good idea (512 MB is ok). This time I decided to also add a small swap partition. While hosting swap on top of the pool itself is a perfectly valid scenario, I actually found it sometime causes issues. Separate partition seems to be slightly better.

blkdiscard -f $DISK

sgdisk --zap-all                      $DISK
sgdisk -n1:1M:+63M  -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+960M  -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+4096M -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:0      -t4:BF00 -c4:Pool $DISK
sgdisk --print                        $DISK

Finally we’re ready to create system ZFS pool. Note that you need to encrypt it at the moment it’s created. And yes, I still like lz4 the best.

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O encryption=aes-256-gcm -O keylocation=prompt -O keyformat=passphrase \
    -O canmount=off -O mountpoint=none -R /mnt/install $POOL $DISK-part4

On top of the pool we can create a root dataset.

zfs create -o canmount=noauto -o mountpoint=/ $POOL/root
zfs mount $POOL/root

Over time I went back and forth whether to use a separate home dataset or not. In this iteration, a separate dataset it is. :)

zfs create -o canmount=noauto -o mountpoint=/home $POOL/home
zfs mount $POOL/home
zfs set canmount=on $POOL/home

Assuming we’re done with datasets, we need to do a last minute setting change.

zfs set devices=off $POOL

Assuming UEFI boot, two additional partitions are needed. One for EFI and one for booting. Unlike what you get with the official guide, here I don’t have ZFS pool for boot partition but a plain old ext4. I find potential fixup works better that way and there is a better boot compatibility. If you are thinking about mirroring, making it bigger and ZFS might be a good idea. For a single disk, ext4 will do.

# yes | mkfs.ext4 $DISK-part2
# mkdir /mnt/install/boot
# mount $DISK-part2 /mnt/install/boot/

# mkfs.msdos -F 32 -n EFI -i 4d65646f $DISK-part1
# mkdir /mnt/install/boot/efi
# mount $DISK-part1 /mnt/install/boot/efi

To start the fun we need debootstrap package. Starting this step, you must be connected to the Internet.

apt update && apt install --yes debootstrap

Bootstrapping Ubuntu on the newly created pool comes next. This will take a while.

debootstrap $(basename `ls -d /cdrom/dists/*/ | grep -v stable | head -1`) /mnt/install/

We can use our live system to update a few files on our new installation.

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

If you are installing via WiFi, you might as well copy your wireless credentials. Don’t worry if this returns errors - that just means you are not using wireless.

mkdir -p /mnt/install/etc/NetworkManager/system-connections/
cp /etc/NetworkManager/system-connections/* /mnt/install/etc/NetworkManager/system-connections/

Finally we’re ready to “chroot” into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install \
    /usr/bin/env DISK=$DISK USER=$USER \
    bash --login

Let’s not forget to setup locale and time zone.

locale-gen --purge "en\_US.UTF-8" # update-locale LANG=en\_US.UTF-8 LANGUAGE=en\_US # dpkg-reconfigure --frontend noninteractive locales

dpkg-reconfigure tzdata

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

Followed by boot environment packages.

apt install --yes zfs-initramfs cryptsetup keyutils grub-efi-amd64-signed shim-signed

Since data is encrypted, we might as well use random key to encrypt our swap.

echo "swap $DISK-part3 /dev/urandom \
    swap,cipher=aes-xts-plain64,size=256,plain" >> /etc/crypttab
cat /etc/crypttab

To mount EFI, boot, and swap partitions, we need to do some fstab setup.

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "/dev/mapper/swap none swap defaults 0 0" >> /etc/fstab
cat /etc/fstab

We might as well activate the swap now.

/etc/init.d/cryptdisks restart && sleep 5
swapon -a

Now we get grub started and update our boot environment.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -u -k $KERNEL

Grub update is what makes EFI tick.

update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy

Finally we install out GUI environment. I personally like ubuntu-desktop-minimal but you can opt for ubuntu-desktop. In any case, it’ll take a considerable amount of time.

apt install --yes ubuntu-desktop-minimal

A short package upgrade will not hurt.

add-apt-repository universe
apt update && apt dist-upgrade --yes

If one is so inclined, /home directory can get a separate dataset too but I usually skip it these days. I just proceed to create the user, assign a few extra groups to it, and make sure its home has correct owner.

adduser --disabled-password --gecos '' $USER
usermod -a -G adm,cdrom,dip,lpadmin,plugdev,sudo $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

As install is ready, we can exit our chroot environment.

exit

And cleanup our mount points.

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

After the reboot you should be able to enjoy your installation.

reboot

PS: There are versions of this guide using the native ZFS encryption for other Ubuntu versions: 21.10 and 20.04

PPS: For LUKS-based ZFS setup, check the following posts: 20.04, 19.10, 19.04, and 18.10.

Encrypted ZFS on Ubuntu 22.10

Ubuntu 22.10 in release notes brought one unpleasant news: “The option to install using zfs as a file system and encryption has been disabled due to a bug”. The official recommendation is to “simply” install Ubuntu 22.04 and upgrade from there. However, if you are willing to go over a lot of manual steps, that’s not necessary.

Those familiar with my previous installation guides will note two things. First, steps below will use LUKS encryption instead of the native ZFS option. I’ve been going back and forth on this one as I do like native ZFS encryption but in setups with hibernation enabled (and this one will be one of those), having both swap with LUKS and native ZFS would require user to type password twice. Not a great user experience.

Second, my personal preferences will “leak through”. This guide will clearly show dislike of snap system and use of way too big swap partition to facilitate hibernation under even the worst-case scenario. Your preferences might vary and thus you might want to adjust guide as necessary. The important part is disk and boot partition setup, everything else is just extra fluff.

Without further ado, let’s proceed with the install.

After booting into Ubuntu desktop installation (via “Try Ubuntu” option) we want to open a terminal. Since all further commands are going to need root credentials, we can start with that.

sudo -i

The very first step should be setting up a few variables - disk, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system. I like to use upper-case for ZFS pool as that’s what will appear as password prompt. It just looks nicer and ZFS doesn’t care either way.

DISK=/dev/disk/by-id/<diskid>
HOST=<hostname>
USER=<username>

For my setup I want 4 partitions. First two partitions will be unencrypted and in charge of booting. While I love encryption, I decided not to encrypt boot partition in order to make my life easier as you cannot integrate boot partition password prompt with the later data password prompt thus requiring you to type password twice. Both swap and ZFS partitions are fully encrypted.

Also, my swap size is way too excessive since I have 64 GB of RAM and I wanted to allow for hibernation under the worst of circumstances (i.e., when RAM is full). Hibernation usually works with much smaller partitions but I wanted to be sure and my disk was big enough to accommodate.

Lastly, while blkdiscard does nice job of removing old data from the disk, I would always recommend also using dd if=/dev/urandom of=$DISK bs=1M status=progress if your disk was not encrypted before.

blkdiscard -f $DISK
sgdisk --zap-all                      $DISK
sgdisk -n1:1M:+127M -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+896M  -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+65G   -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:0      -t4:8309 -c4:ZFS  $DISK
sgdisk --print                        $DISK

Once partitions are created, we want to setup our LUKS encryption.

cryptsetup luksFormat -q --type luks2 \
    --cipher aes-xts-plain64 --key-size 512 \
    --pbkdf argon2i $DISK-part4
cryptsetup luksFormat -q --type luks2 \
    --cipher aes-xts-plain64 --key-size 512 \
    --pbkdf argon2i $DISK-part3

Since creating encrypted partition doesn’t mount them, we do need this as a separate step. Notice I use host name as the name of the main data partition.

cryptsetup luksOpen $DISK-part4 $HOST
cryptsetup luksOpen $DISK-part3 swap

Finally, we can setup our ZFS pool with an optional step of setting quota to roughly 80% of disk capacity. Adjust exact value as needed.

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O canmount=off -O mountpoint=none -R /mnt/install ${HOST^} /dev/mapper/$HOST
zfs set quota=1.5T ${HOST^}

I used to be fan of using just a main dataset for everything but these days I use more conventional “separate root dataset” approach.

zfs create -o canmount=noauto -o mountpoint=/ ${HOST^}/Root
zfs mount ${HOST^}/Root

And a separate home partition will not be forgotted.

zfs create -o canmount=noauto -o mountpoint=/home ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home

With all datasets in place, we can finish setting the main dataset properties.

zfs set devices=off ${HOST^}

Now it’s time to format swap.

mkswap /dev/mapper/swap

And then boot partition.

yes | mkfs.ext4 $DISK-part2
mkdir /mnt/install/boot
mount $DISK-part2 /mnt/install/boot

And finally EFI partition.

mkfs.msdos -F 32 -n EFI -i 4d65646f $DISK-part1
mkdir /mnt/install/boot/efi
mount $DISK-part1 /mnt/install/boot/efi

At this time, I also like to disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional.

sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need debootstrap package. Starting this step, you must be connected to the Internet.

apt update && apt install --yes debootstrap

Bootstrapping Ubuntu on the newly created pool comes next. This will take a while.

debootstrap $(basename `ls -d /cdrom/dists/*/ | grep -v stable | head -1`) /mnt/install/

We can use our live system to update a few files on our new installation.

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

If you are installing via WiFi, you might as well copy your wireless credentials. Don’t worry if this returns errors - that just means you are not using wireless.

mkdir -p /mnt/install/etc/NetworkManager/system-connections/
cp /etc/NetworkManager/system-connections/* /mnt/install/etc/NetworkManager/system-connections/

Atlast, we’re ready to “chroot” into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install \
    /usr/bin/env DISK=$DISK USER=$USER \
    bash --login

With our newly installed system running, let’s not forget to setup locale and time zone.

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

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

Followed by the boot environment packages.

apt install --yes zfs-initramfs cryptsetup keyutils grub-efi-amd64-signed shim-signed

Now it’s time to setup crypttab so our encrypted partitions are decrypted on boot.

echo "$HOST UUID=$(blkid -s UUID -o value $DISK-part4) none \
    luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "swap UUID=$(blkid -s UUID -o value $DISK-part3) none \
    swap,luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
cat /etc/crypttab

To mount all those partitions, we need also some fstab entries. The last entry is not strictly needed. I just like to add it in order to hide our LUKS encrypted ZFS from file manager.

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "UUID=$(blkid -s UUID -o value /dev/mapper/swap) \
    none swap defaults 0 0" >> /etc/fstab
echo "/dev/disk/by-uuid/$(blkid -s UUID -o value /dev/mapper/$HOST) \
    none auto nosuid,nodev,nofail 0 0" >> /etc/fstab
cat /etc/fstab

If hibernation is desired, a few settings need to be added.

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/' \
    /etc/systemd/sleep.conf

Since we’re doing laptop setup, we can also setup lid switch. I like to set it as suspend-then-hibernate.

sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/' \
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=suspend-then-hibernate/' \
    /etc/systemd/logind.conf

Lastly, I adjust swappiness a bit.

echo "vm.swappiness=10" >> /etc/sysctl.conf

And then, we can get grub going. Do note we also setup booting from swap (hibernation support) here too.

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    nvme.noacpi=1 \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/swap)\"/" \
    /etc/default/grub
update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy
update-initramfs -u -k all

Next step is to create boot environment.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -c -k $KERNEL

Finally, we can install our desktop environment.

apt install --yes ubuntu-desktop-minimal

Once installation is done, I like to remove snap and banish it from ever being installed.

apt remove --yes snapd
echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd

Since Firefox is only available as snapd package, we can install it manually.

add-apt-repository --yes ppa:mozillateam/ppa
cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF
apt update && apt install --yes firefox

While at it, I might as well get Chrome too.

cd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install ./google-chrome-stable_current_amd64.deb

For Framework Laptop I use here, we need one more adjustment due to Dell audio needing a special care. Note that owners of Gen12 boards need a few more adjustments.

echo "options snd-hda-intel model=dell-headset-multi" >> /etc/modprobe.d/alsa-base.conf

Of course, we need to have a user too.

adduser --disabled-password --gecos '' $USER
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

I like to add some extra packages and do one final upgrade before calling it done.

add-apt-repository --yes universe
apt update && apt dist-upgrade --yes

It took a while but we can finally exit our debootstrap environment.

exit

Let’s clean all mounted partitions and get ZFS ready for next boot.

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

With reboot, we should be done and our new system should boot with a password prompt.

reboot

Once we log into it, we need to adjust boot image and test hibernation. If you see your desktop after waking it up, all is good.

sudo update-initramfs -u -k all
sudo systemctl hibernate

If you get Failed to hibernate system via logind: Sleep verb "hibernate" not supported go into BIOS and disable secure boot (Enforce Secure Boot option). Unfortunately, secure boot and hibernation still don’t work together but there is some work in progress to make it happen in future. At this time, you need to select one or the other.


PS: If you already installed system with secure boot and hibernation is not working, run update-initramfs -c -k all and try again.

PPS: There are versions of this guide using the native ZFS encryption for other Ubuntu versions: 22.04, 21.10, and 20.04.

PPPS: For LUKS-based ZFS setup, check the following posts: 20.04, 19.10, 19.04, and 18.10.

Ubuntu 23.04 on Framework Laptop (with Hibernate)

I’ve been running Ubuntu on my Framework 13 for a while now without any major issues. However, my initial setup restricted me to a deep sleep suspend that will drain your battery in a day or two if you forget about it. As I anyhow needed to reinstall my system to get Ubuntu 23.04 going, I decided to mix it up a bit.

My setup is simple and has only a few requirements. First of all, a full disk encryption is a must. Secondly, ZFS is non-negotiable. And lastly, it would be nice to have hibernation this time round.

When it comes to full disk encryption with ZFS, there is an option of native ZFS encryption. And indeed, I’ve done setups with it before. However, getting hibernation running on top of ZFS was not something I managed to get running properly.

For hibernation, I really prefer to have a separate swap partition encrypted using Luks. And, if you use both Luks and native ZFS encryption, you get asked for the encryption passphrase twice. Since I’m too lazy for that, I decided to have ZFS on top of the Luks, like in the good old days. Performance-wise it’s awash anyhow. Yes, writing is a bit slower on artificial tests but in reality, the difference is negligible.

Avid readers of my previous installation guides will already know that my personal preferences are really noticeable in these guides. For example, I like my partitions set up a certain way and I will always nuke the dreadful snap system.

Honestly, if you are ok with the default Ubuntu setup, or just uncomfortable with the command line, you might want to stop reading and simply follow the official Framework 13 installation guide. It’s a great guide and the final result is something 99% of people will be happy with.

The first step is to boot into the “Try Ubuntu” option of the USB installation. Once we have a desktop, we want to open a terminal. And, since all further commands are going to need root credentials, we can start with that.

sudo -i

Next step should be setting up a few variables - disk, pool name, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK=/dev/disk/by-id/<diskid>
POOL=<poolname>
HOST=<hostname>
USER=<username>

For this setup, I wanted 4 partitions. The first two partitions will be unencrypted and in charge of booting. While I love encryption, I decided not to encrypt the boot partition in order to make my life easier as you cannot integrate the boot partition password prompt with the later data password prompt thus requiring you to type the password twice (or trice if you decide to use native ZFS encryption on top of that). Both swap and ZFS partition are fully encrypted.

Also, my swap size is way too excessive since I have 64 GB of RAM and I wanted to allow for hibernation under the worst of circumstances (i.e., when RAM is full). Hibernation usually works with much smaller partitions but I wanted to be sure and my disk

Also, my swap size is way too excessive since I have 64 GB of RAM and I wanted to allow for hibernation under the worst of circumstances (i.e., when RAM is full). Hibernation usually works with much smaller partitions but I wanted to be sure and my disk was big enough to accommodate.

Lastly, while blkdiscard does nice job of removing old data from the disk, I would always recommend also using dd if=/dev/urandom of=$DISK bs=1M status=progress if your disk was not encrypted before.

blkdiscard -f $DISK
sgdisk --zap-all                     $DISK
sgdisk -n1:1M:+63M -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+960M -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+64G  -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:0     -t4:8309 -c4:ZFS  $DISK
sgdisk --print                       $DISK

Once partitions are created, we want to setup our LUKS encryption. Here you will notice I use luks2 headers with a few arguments helping with nVME performance.

cryptsetup luksFormat -q --type luks2 \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK-part4

cryptsetup luksFormat -q --type luks2 \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK-part3

Since creating encrypted partition doesn’t mount them, we do need this as a separate step. Since the swap partition will be the first one to load, I will give it a name of the host in order to have a bit nicer password prompt.

cryptsetup luksOpen $DISK-part4 zfs
cryptsetup luksOpen $DISK-part3 $HOST

Finally, we can set up our ZFS pool with an optional step of setting quota to roughly 80% of disk capacity. Adjust the exact values as needed.

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    $POOL /dev/mapper/zfs
zfs set quota=1.5T $POOL

I used to be a fan of using just a main dataset for everything, but these days I use a more conventional “separate root dataset” approach.

zfs create -o canmount=noauto -o mountpoint=/ $POOL/Root
zfs mount $POOL/Root

And a separate home partition will not be forgotten.

zfs create -o canmount=noauto -o mountpoint=/home $POOL/Home
zfs mount $POOL/Home
zfs set canmount=on $POOL/Home

With all datasets in place, we can finish setting the main dataset properties.

zfs set devices=off $POOL

Now it’s time to format the swap.

mkswap /dev/mapper/$HOST

And then the boot partition.

yes | mkfs.ext4 $DISK-part2
mkdir /mnt/install/boot
mount $DISK-part2 /mnt/install/boot

And finally, the EFI partition.

mkfs.msdos -F 32 -n EFI -i 4d65646f $DISK-part1
mkdir /mnt/install/boot/efi
mount $DISK-part1 /mnt/install/boot/efi

At this time, I also sometime disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional.

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need debootstrap package. Starting this step, you must be connected to the Internet.

apt update && apt install --yes debootstrap

Bootstrapping Ubuntu on the newly created pool comes next. This will take a while.

debootstrap lunar /mnt/install/

We can use our live system to update a few files on our new installation.

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

If you are installing via WiFi, you might as well copy your wireless credentials. Don’t worry if this returns errors - that just means you are not using wireless.

mkdir -p /mnt/install/etc/NetworkManager/system-connections/
cp /etc/NetworkManager/system-connections/* /mnt/install/etc/NetworkManager/system-connections/

At last, we’re ready to “chroot” into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install \
    /usr/bin/env DISK=$DISK USER=$USER \
    bash --login

With our newly installed system running, let’s not forget to set up locale and time zone.

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

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends \
    linux-image-generic linux-headers-generic

Followed by the boot environment packages.

apt install --yes \
    zfs-initramfs cryptsetup keyutils grub-efi-amd64-signed shim-signed

Now we set up crypttab so our encrypted partitions are decrypted on boot.

echo "$HOST PARTUUID=$(blkid -s PARTUUID -o value $DISK-part3) none \
      swap,luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "zfs PARTUUID=$(blkid -s PARTUUID -o value $DISK-part4) none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
cat /etc/crypttab

To mount all those partitions, we also need some fstab entries. The last entry is not strictly needed. I just like to add it in order to hide our LUKS encrypted ZFS from the file manager.

echo "UUID=$(blkid -s UUID -o value /dev/mapper/$HOST) \
      swap swap defaults 0 0" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part2) \
      /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part1) \
      /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "/dev/disk/by-uuid/$(blkid -s UUID -o value /dev/mapper/zfs) \
      none auto nosuid,nodev,nofail 0 0" >> /etc/fstab
cat /etc/fstab

On systems with a lot of RAM, I like to adjust swappiness a bit. This is inconsequential in the grand scheme of things, but I like to do it anyhow.

echo "vm.swappiness=10" >> /etc/sysctl.conf

Now we create the boot environment.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -c -k $KERNEL

And then, we can get grub going. Do note we also set up booting from swap (needed for hibernation) here too. Since we’re using secure boot, bootloaded-id HAS to be Ubuntu.

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    nvme.noacpi=1 \
    module_blacklist=hid_sensor_hub \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$HOST)\"/" \
    /etc/default/grub
update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy

And now, finally, we can install our desktop environment.

apt install --yes ubuntu-desktop-minimal

Once the installation is done, I like to remove snap and banish it from ever being installed.

apt remove --yes snapd
echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd

Since Firefox is only available as snapd package, we can install it manually.

add-apt-repository --yes ppa:mozillateam/ppa
cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF
apt update && apt install --yes firefox

For Framework Laptop I use here, we need one more adjustment due to Dell audio needing special care. In addition, you might want to mess with WiFi power save modes a bit.

echo "options snd-hda-intel model=dell-headset-multi" >> /etc/modprobe.d/alsa-base.conf
sed '/s/wifi.powersave =.*/wifi.powersave = 2/' \
    /etc/NetworkManager/conf.d/default-wifi-powersave-on.conf

Of course, we need to have a user too.

adduser --disabled-password --gecos '' $USER
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

I like to add some extra packages and do one final upgrade before dealing with the sleep stuff.

add-apt-repository --yes universe
apt update && apt dist-upgrade --yes

The first portion is setting up the whole suspend-then-hibernate stuff. This will make Ubuntu to do normal suspend first. If suspended for 20 minutes, it will quickly wake up and do the hibernation then.

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=20min/' \
    /etc/systemd/sleep.conf

And lastly, the whole sleep setup is nothing if we cannot activate it. Closing the lid seems like a perfect place to do it.

apt install -y pm-utils

sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/' \
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=suspend-then-hibernate/' \
    /etc/systemd/logind.conf

It took a while but we can finally exit our debootstrap environment.

exit

Let’s clean all mounted partitions and get ZFS ready for next boot.

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

After reboot, we should be done and our new system should boot with a password prompt.

reboot

Once we log into it, we still need to adjust boot image and grub, followed by a hibernation test. If you see your desktop in the same state as you left it after waking the computer up, all is good.

sudo update-initramfs -u -k all
sudo update-grub
sudo systemctl hibernate

If you get Failed to hibernate system via logind: Sleep verb "hibernate" not supported, go into BIOS and disable secure boot (Enforce Secure Boot option). Unfortunately, the secure boot and hibernation still don’t work together but there is some work in progress to make it happen in the future. At this time, you need to select one or the other.


PS: Just setting HibernateDelaySec as in older Ubuntu versions doesn’t work with the current Ubuntu anymore due to systemd bug. Hibernation is only going to happen when the battery reaches 5% of capacity instead of at a predefined time. This was corrected in v253 but I doubt Ubuntu 23.04 will get that update. I’ll leave it in the guide as it’ll likely work again in Ubuntu 23.10.

PPS: If battery life is really precious to you, you can go to hibernate directly by setting HandleLidSwitch=suspend-then-hibernate. Alternatively, you can look into setting mem_sleep_default=deep in the Grub.

PPPS: There are versions of this guide (without hibernation though) using the native ZFS encryption for the other Ubuntu versions: 22.04, 21.10, and 20.04. For LUKS-based ZFS setup, check the following posts: 22.10, 20.04, 19.10, 19.04, and 18.10.

Mirrored ZFS on Ubuntu 23.10

One reason why I was excited about Framework 16 was to get two NVMe slots. While I was slightly disappointed by the fact the second slot could only handle 2230 M.2 SSD (instead of the full size 2280), having two slots makes dual boot easier to deal with. Alternatively, for ZFS aficionados like myself, it allows for data mirroring.

With both M.2 slots filled, I decided to set up UEFI boot ZFS mirror with LUKS-based encryption. Yes, I know that native encryption exists on ZFS and it might even have some advantages when it comes to performance.

Another thing you’ll notice about my installation procedure is the number of manual steps. While you can use the normal installer and then add mirroring later, I generally like manual installation better as it gives me freedom to set up partitions as I like them.

Lastly, I am going with Ubuntu 23.10 which is not officially supported by Framework. I found it works for me, but your mileage may vary.

With that out of the way, we can start installation by booting from USB, going to terminal, and becoming root:

sudo -i

Now we can set up a few variables - disk, pool, host name, and user name. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK1=/dev/disk/by-id/<firstdiskid>
DISK2=/dev/disk/by-id/<seconddiskid>
POOL=mypool
HOST=myhost
USER=myuser

The general idea of my disk setup is to maximize the amount of space available for the pool with the minimum of supporting partitions. However, you will find these partitions are a bit larger than what you can see at other places - especially when it comes to the boot and swap partitions. You can reduce either but I found having them oversized is beneficial for future proofing. Also, I intentionally make both the EFI and boot partition share the same UUID. This will come in handy later. And yes, you need swap partition no matter how much RAM you have (unless you really hate the hibernation).

In either case, we can create them all:

DISK1_ENDSECTOR=$(( `blockdev --getsz $DISK1` / 2048 * 2048 - 2048 - 1 ))
DISK2_ENDSECTOR=$(( `blockdev --getsz $DISK2` / 2048 * 2048 - 2048 - 1 ))

blkdiscard -f $DISK1 2>/dev/null
sgdisk --zap-all                                $DISK1
sgdisk -n1:1M:+63M            -t1:EF00 -c1:EFI  $DISK1
sgdisk -n2:0:+1984M           -t2:8300 -c2:Boot $DISK1
sgdisk -n3:0:+64G             -t3:8200 -c3:Swap $DISK1
sgdisk -n4:0:$DISK1_ENDSECTOR -t4:8309 -c4:LUKS $DISK1
sgdisk --print                                  $DISK1

PART1UUID=`blkid -s PARTUUID -o value $DISK1-part1`
PART2UUID=`blkid -s PARTUUID -o value $DISK1-part2`

blkdiscard -f $DISK2 2>/dev/null
sgdisk --zap-all                                               $DISK2
sgdisk -n1:1M:+63M            -t1:EF00 -c1:EFI  -u1:$PART1UUID $DISK2
sgdisk -n2:0:+1984M           -t2:8300 -c2:Boot -u2:$PART2UUID $DISK2
sgdisk -n3:0:+64G             -t3:8200 -c3:Swap -u3:R          $DISK2
sgdisk -n4:0:$DISK2_ENDSECTOR -t4:8309 -c4:LUKS -u4:R          $DISK2
sgdisk --print                                                 $DISK2

Since I use LUKS, I get to encrypt my ZFS partition now.

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK1-part4

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK2-part4

Of course, encrypting swap is needed too. Here I use the same password as one I used for data. Why? Because that way you get to unlock them both with a single password prompt. Of course, if you wish, you can have different password too.

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK1-part3

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK2-part3

Now we decrypt all those partitions so we can fill them with sweet, sweet data.

cryptsetup luksOpen $DISK1-part4 ${DISK1##*/}-part4
cryptsetup luksOpen $DISK2-part4 ${DISK2##*/}-part4

cryptsetup luksOpen $DISK1-part3 ${DISK1##*/}-part3
cryptsetup luksOpen $DISK2-part3 ${DISK2##*/}-part3

Finally, we can create our mirrored pool and any datasets you might want. I usually have a few more but root (/) and home (/home) partition are minimum:

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    $POOL mirror /dev/mapper/${DISK1##*/}-part4 /dev/mapper/${DISK2##*/}-part4

zfs create -o canmount=noauto -o mountpoint=/ \
           -o reservation=64G \
           ${HOST^}/System
zfs mount ${HOST^}/System

zfs create -o canmount=noauto -o mountpoint=/home \
           -o quota=128G \
           ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home

zfs set devices=off ${HOST^}

Now we can format swap partition:

mkswap /dev/mapper/${DISK1##*/}-part3
mkswap /dev/mapper/${DISK2##*/}-part3

Assuming UEFI boot, I like to have ext4 partition here instead of more common ZFS pool as having encryption makes it overly complicated otherwise.

yes | mkfs.ext4 $DISK1-part2
mkdir /mnt/install/boot
mount $DISK1-part2 /mnt/install/boot/

Lastly, we need to format EFI partition:

mkfs.msdos -F 32 -n EFI -i 4d65646f $DISK1-part1
mkdir /mnt/install/boot/efi
mount $DISK1-part1 /mnt/install/boot/efi

And, only now we’re ready to copy system files. This will take a while.

apt update
apt install --yes debootstrap
debootstrap mantic /mnt/install/

Before using our newly copied system to finish installation, we can set a few files.

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

Finally, we can login into our new semi-installed system using chroot:

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env \
    DISK1=$DISK1 DISK2=$DISK2 HOST=$HOST USER=$USER \
    bash --login

My next step is usually setting up locale and time-zone. Since I sometimes dual-boot, I found using local time in BIOS works the best.

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

ln -sf /usr/share/zoneinfo/PST8PDT  /etc/localtime
dpkg-reconfigure -f noninteractive tzdata

echo UTC=no >> /etc/default/rc5

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

To allow for decrypting, we need to update crypttab:

echo "${DISK1##*/}-part4 $DISK1-part4 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "${DISK1##*/}-part3 $DISK1-part3 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "${DISK2##*/}-part4 $DISK2-part4 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "${DISK2##*/}-part3 $DISK2-part3 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
cat /etc/crypttab

And, of course, all those drives need to be mounted too. Please note that the last two entries are not really needed, but I like to have them as it prevents Ubuntu from cluttering the taskbar otherwise.

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK1-part2) \
    /boot ext4 nofail,noatime,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK1-part1) \
    /boot/efi vfat nofail,noatime,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "/dev/mapper/${DISK1##*/}-part3 \
    swap swap nofail 0 0" >> /etc/fstab
echo "/dev/mapper/${DISK2##*/}-part3 \
    swap swap nofail 0 0" >> /etc/fstab
echo "/dev/mapper/${DISK1##*/}-part4 \
    none auto nofail,nosuid,nodev,noauto 0 0" >> /etc/fstab
echo "/dev/mapper/${DISK2##*/}-part4 \
    none auto nofail,nosuid,nodev,noauto 0 0" >> /etc/fstab
cat /etc/fstab

Next we can proceed with setting up the boot environment:

apt install --yes zfs-initramfs cryptsetup keyutils grub-efi-amd64-signed shim-signed

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -c -k all

To be able to actually use that boot environment, we install Grub too:

apt install --yes grub-efi-amd64-signed shim-signed
sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/${DISK1##*/}-part3)\"/" \
    /etc/default/grub
update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    --bootloader-id=Ubuntu --recheck --no-floppy

With most of the system setup done, we get to install (minimum) Desktop packages:

apt install --yes ubuntu-desktop-minimal

To ensure the system wakes up with firewall, you can get iptables running:

apt install --yes man iptables iptables-persistent

iptables -F
iptables -X
iptables -Z
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

ip6tables -F
ip6tables -X
ip6tables -Z
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -p ipv6-icmp -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT

netfilter-persistent save
echo ; iptables -L ; echo; ip6tables -L

Some people like the snap packaging system and those people are wrong. If you are one of those that share this belief, you can remove snap too:

apt remove --yes snapd
echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd

Since our snap removal also got rid of Firefox, we can add it manually:

add-apt-repository --yes ppa:mozillateam/ppa
cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF
apt update && apt install --yes firefox

To have a bit wider software selection, adding universe repo comes in handy:

add-apt-repository --yes universe
apt update

And, since Framework 16 is AMD-based, adding AMD PPA is a must:

add-apt-repository --yes ppa:superm1/ppd
apt update

Also, since Framework 16 is new, we need to update the keyboard definition too (this step might not be necessary in the future):

cat << EOF | sudo tee -a /usr/share/libinput/50-framework.quirks
[Framework Laptop 16 Keyboard Module]
MatchName=Framework Laptop 16 Keyboard Module*
MatchUdevType=keyboard
MatchDMIModalias=dmi:*svnFramework:pnLaptop16*
AttrKeyboardIntegration=internal
EOF

Fully optional is also setup for hibernation. It starts with setting up the sleep configuration:

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \\
    /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \\
    /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \\
    /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/' \\
    /etc/systemd/sleep.conf

And continues with button setup:

apt install -y pm-utils

sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/' \\
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/' \\
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=suspend-then-hibernate/' \\
    /etc/systemd/logind.conf

With all system stuff done, we finally get to create our new user:

adduser --disabled-password --gecos '' -u $USERID $USER
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

Now we can exit back into the installer:

exit

Don’t forget to properly clean our mount points in order to have the system boot:

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

Finally, we are just a reboot away from success:

reboot

Once we login, there are just a few finishing touches. For example, I like to increase text size:

gsettings set org.gnome.desktop.interface text-scaling-factor 1.25

If you still remember where we started, you’ll notice that, while data is mirrored, our EFI and boot partition are not. My preferred way of keeping them in sync is by using my own utility named syncbootpart:

wget -O- http://packages.medo64.com/keys/medo64.asc | sudo tee /etc/apt/trusted.gpg.d/medo64.asc
echo "deb http://packages.medo64.com/deb stable main" | sudo tee /etc/apt/sources.list.d/medo64.list
sudo apt-get update
sudo apt-get install -y syncbootpart
sudo syncbootpart

sudo update-initramfs -u -k all
sudo update-grub

This utility will find what your currently used boot and EFI partition are and copy it to the second disk (using UUID in order to match them). And, every time a new kernel is installed, it will copy it to the second disk too. Since both disks share UUID, BIOS will boot from whatever it finds first and you can lose either drive while preserving your “bootability”.

At last, with all manual steps completed, we can enjoy our new system.

Ubuntu 24.04 ZFS Mirror on Framework 16 Laptop (with Hibernate)

ZFS was, and still is, the primary driver for my Linux adventures. Be it snapshots or seamless data restoration, once you go ZFS it’s really hard to go back. And to get the full benefits of ZFS setup you need at least two drives. Since my Framework 16 came with two drives, the immediate idea was to setup ZFS mirror.

While Framework 16 does have two NVMe drives, they are not the same. One of them is full-size M.2 2280 slot and that’s the one I love. The other one is rather puny 2230 in size. Since M.2 2230 SSDs are limited to 2 TB in size, that also puts an upper limit on our mirror size. However, I still decided to combine it with a 4 TB drive.

My idea for setup is as follows: I match the smaller drive partitioning exactly so I can have myself as much mirrored disk space as possible. Leftover space I get to use for files that are more forgiving when it comes to a data loss.

I also wanted was a full disk encryption using LUKS (albeit most of the steps work if you have native ZFS encryption too). Since this is a laptop, I definitely wanted hibernation support too as it makes life much easier.

Now, easy and smart approach might be to use Ubuntu’s ZFS installer directly and let it sort everything out. And let nobody tell you anything is wrong with that. However, I personally like a bit more controlled approach that requires a lot of manual steps. And no, I don’t remember them by heart - I just do a lot of copy/paste.

With that out of the way, let’s go over the necessary steps.

The first step is to boot into the “Try Ubuntu” option of the USB installation. Once we have a desktop, we want to open a terminal. And, since all further commands are going to need root access, we can start with that.

sudo -i

Next step should be setting up a few variables - disk, pool name, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK1=/dev/disk/by-id/<disk1>
DISK2=/dev/disk/by-id/<disk2>
HOST=<hostname>
USERNAME=<username>

On a smaller drive I wanted 3 partitions. The first two partitions are unencrypted and in charge of booting. While I love encryption, I almost never encrypt the boot partition in order to make my life easier as you cannot seamlessly integrate the boot partition password prompt with the later password prompt thus requiring you to type the password twice (or thrice if you decide to use native ZFS encryption on top of that). Third partition would be encrypted and take the rest of the drive.

On bigger drive I decided to have 5 partitions. First three would match the smaller drive. Fourth partition is 96 GB swap in order to accommodate full the worst case scenario. Realistically, even though my laptop has 96 GB of RAM, I could have gone with a smaller swap partition but I decided to reserve this space for potential future adventures. The last partition will be for extra non-mirrored data.

All these requirements come in the following few partitioning commands:

DISK1_LASTSECTOR=$(( `blockdev --getsz $DISK1` / 2048 * 2048 - 2048 - 1 ))
DISK2_LASTSECTOR=$(( `blockdev --getsz $DISK2` / 2048 * 2048 - 2048 - 1 ))

blkdiscard -f $DISK1 2>/dev/null
sgdisk --zap-all                                 $DISK1
sgdisk -n1:1M:+127M            -t1:EF00 -c1:EFI  $DISK1
sgdisk -n2:0:+1920M            -t2:8300 -c2:Boot $DISK1
sgdisk -n3:0:$DISK1_LASTSECTOR -t3:8309 -c3:LUKS $DISK1
sgdisk --print                                   $DISK1

PART1UUID=`blkid -s PARTUUID -o value $DISK1-part1`
PART2UUID=`blkid -s PARTUUID -o value $DISK1-part2`

blkdiscard -f $DISK2 2>/dev/null
sgdisk --zap-all                                                $DISK2
sgdisk -n1:1M:+127M            -t1:EF00 -c1:EFI  -u1:$PART1UUID $DISK2
sgdisk -n2:0:+1920M            -t2:8300 -c2:Boot -u2:$PART2UUID $DISK2
sgdisk -n3:0:$DISK1_LASTSECTOR -t3:8309 -c3:LUKS -u3:R          $DISK2
sgdisk -n4:0:+96G              -t4:8200 -c4:Swap -u4:R          $DISK2
sgdisk -n5:0:$DISK2_LASTSECTOR -t5:8309 -c5:LUKS                $DISK2
sgdisk --print                                                  $DISK2

And yes, using the same partition UUIDs for boot drives is important and we’ll use it later to have a mirror of our boot data too.

The next step is to setup all LUKS partitions. If you paid attention, that means we need to repeat formatting a total of 4 times. Unless you want to deal with multiple password prompts, make sure to use the same password for each:

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK1-part3

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK2-part3

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK2-part4

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK2-part5

Since creating encrypted partitions doesn’t mount them, we do need this as a separate step. I like to name my LUKS devices based on partition names so we can recognize them more easily:

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    $DISK1-part3 ${DISK1##*/}-part3
cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    $DISK2-part3 ${DISK2##*/}-part3
cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    $DISK2-part4 ${DISK2##*/}-part4
cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    $DISK2-part5 ${DISK2##*/}-part5

Finally, we can set up our mirrored ZFS pool with an optional step of setting quota to roughly 85% of disk capacity. Since we’re using LUKS, there’s no need to setup any ZFS keys. Name of the mirrored pool will match name of the host and it will contain several datasets to start with. It’s a good starting point, adjust as needed:

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O quota=1600G \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    ${HOST^} mirror /dev/mapper/${DISK1##*/}-part3 /dev/mapper/${DISK2##*/}-part3

zfs create -o canmount=noauto -o mountpoint=/ \
    -o reservation=100G \
    ${HOST^}/System
zfs mount ${HOST^}/System

zfs create -o canmount=noauto -o mountpoint=/home \
           ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home

zfs create -o canmount=noauto -o mountpoint=/Data \
           ${HOST^}/Data
zfs set canmount=on ${HOST^}/Data

zfs set devices=off ${HOST^}

Of course, we can also setup our extra non-mirrored pool:

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O quota=1600G \
    -O canmount=on -O mountpoint=/Extra \
    ${HOST^}Extra /dev/mapper/${DISK2##*/}-part5

With ZFS done, we might as well setup boot, EFI, and swap partitions too. Any yes, we don’t have mirrored boot and EFI at this time; we’ll sort that out later.

yes | mkfs.ext4 $DISK1-part2
mkdir /mnt/install/boot
mount $DISK1-part2 /mnt/install/boot/

mkfs.msdos -F 32 -n EFI -i 4d65646f $DISK1-part1
mkdir /mnt/install/boot/efi
mount $DISK1-part1 /mnt/install/boot/efi

mkswap /dev/mapper/${DISK2##*/}-part4

At this time, I also sometimes disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional.

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need to debootstrap our OS. As of this step, you must be connected to the Internet.

apt update
apt dist-upgrade --yes
apt install --yes debootstrap
debootstrap noble /mnt/install/

We can use our live system to update a few files on our new installation:

echo $HOST > /mnt/install/etc/hostname
sed "s/ubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
rm /mnt/install/etc/apt/sources.list
cp /etc/apt/sources.list.d/ubuntu.sources /mnt/install/etc/apt/sources.list.d/ubuntu.sources
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

At last, we’re ready to chroot into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env \
    DISK1=$DISK1 DISK2=$DISK2 HOST=$HOST USERNAME=$USERNAME \
    bash --login

With our newly installed system running, let’s not forget to set up locale and time zone.

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

ln -sf /usr/share/zoneinfo/America/Los_Angeles /etc/localtime
dpkg-reconfigure -f noninteractive tzdata

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends \
    linux-image-generic linux-headers-generic

Now we set up crypttab so our encrypted partitions are decrypted on boot.

echo "${DISK1##*/}-part3 $DISK1-part3 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "${DISK2##*/}-part3 $DISK2-part3 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

echo "${DISK2##*/}-part4 $DISK2-part4 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

echo "${DISK2##*/}-part5 $DISK2-part5 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

cat /etc/crypttab

To mount all those partitions, we also need some fstab entries. ZFS entries are not strictly needed. I just like to add them in order to hide our LUKS encrypted ZFS from the file manager:

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK1-part2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK1-part1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab

echo "/dev/mapper/${DISK1##*/}-part3 \
    none auto nofail,nosuid,nodev,noauto 0 0" >> /etc/fstab
echo "/dev/mapper/${DISK2##*/}-part3 \
    none auto nofail,nosuid,nodev,noauto 0 0" >> /etc/fstab

echo "/dev/mapper/${DISK2##*/}-part4 \
    swap swap nofail 0 0" >> /etc/fstab

echo "/dev/mapper/${DISK2##*/}-part5 \
    none auto nofail,nosuid,nodev,noauto 0 0" >> /etc/fstab

cat /etc/fstab

On systems with a lot of RAM, I like to adjust memory settings a bit. This is inconsequential in the grand scheme of things, but I like to do it anyway.

echo "vm.swappiness=10" >> /etc/sysctl.conf
echo "vm.min_free_kbytes=1048576" >> /etc/sysctl.conf

Now we can create the boot environment:

apt install --yes zfs-initramfs cryptsetup keyutils grub-efi-amd64-signed shim-signed
update-initramfs -c -k all

And then, we can get grub going. Do note we also set up booting from swap (needed for hibernation) here too. If you’re using secure boot, bootloaded-id HAS to be Ubuntu.

apt install --yes grub-efi-amd64-signed shim-signed
sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/${DISK2##*/}-part4)\"/" \
    /etc/default/grub
update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    --bootloader-id=Ubuntu --recheck --no-floppy

I don’t like snap so I preemptively banish it from ever being installed:

apt remove --yes snapd 2>/dev/null
echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd
apt update

And now, finally, we can install our desktop environment.

apt install --yes ubuntu-desktop-minimal man

Since Firefox is a snapd package (banished), we can install it manually:

add-apt-repository --yes ppa:mozillateam/ppa
cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF
apt update && apt install --yes firefox

Chrome aficionados, can install it too:

pushd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install ./google-chrome-stable_current_amd64.deb
popd

If you still remember the start of this post, we are yet to mirror our boot and EFI partition. For this, I have a small utility we might as well install now:

wget -O- http://packages.medo64.com/keys/medo64.asc | sudo tee /etc/apt/trusted.gpg.d/medo64.asc
echo "deb http://packages.medo64.com/deb stable main" | sudo tee /etc/apt/sources.list.d/medo64.list
apt update
apt install -y syncbootpart
syncbootpart

With Framework 16, there are no mandatory changes you need to do in order to have the system working. That said, I still like to do a few changes; the first of them is to allow trim operation on expansion cards:

cat << EOF | tee /etc/udev/rules.d/42-framework-storage.rules
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="13fe", ATTRS{idProduct}=="6500", ATTR{provisioning_mode}:="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0005", ATTR{provisioning_mode}:="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0010", ATTR{provisioning_mode}:="unmap"
EOF

Since we’re doing hibernation, we might as well disable some wake up events that might interfere. I explain the exact process in another blog post but suffice it to say, this works for me:

cat << EOF | sudo tee /etc/udev/rules.d/42-disable-wakeup.rules
ACTION=="add", SUBSYSTEM=="i2c", DRIVER=="i2c_hid_acpi", ATTRS{name}=="PIXA3854:00", ATTR{power/wakeup}="disabled"
ACTION=="add", SUBSYSTEM=="pci", DRIVER=="xhci_hcd", ATTRS{subsystem_device}=="0x0001", ATTRS{subsystem_vendor}=="0xf111", ATTR{power/wakeup}="disabled"
ACTION=="add", SUBSYSTEM=="serio", DRIVER=="atkbd", ATTR{power/wakeup}="disabled"
ACTION=="add", SUBSYSTEM=="usb", DRIVER=="usb", ATTR{power/wakeup}="disabled"
EOF

[2025-01-11] Probably better way is creating a small pre-hibernate script that will hunt down these devices:

cat << EOF | sudo tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "button" ] || [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF

sudo chmod +x /usr/lib/systemd/system-sleep/framework

For hibernation I like to change sleep settings so that hibernation kicks in after 13 minutes of sleep:

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/' \
    /etc/systemd/sleep.conf

For that we also need to do a minor lid switch configuration adjustment:

apt install -y pm-utils

sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/' \
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/' \
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=suspend-then-hibernate/' \
    /etc/systemd/logind.conf

Lastly, we need to have a user too.

adduser --disabled-password --gecos '' $USERNAME
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USERNAME
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
passwd $USERNAME

It took a while, but we can finally exit our debootstrap environment:

exit

Let’s clean all mounted partitions and get ZFS ready for next boot:

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

After reboot, we should be done and our new system should boot with a password prompt.

reboot

Once we log into it, I like to first increase text size a bit:

gsettings set org.gnome.desktop.interface text-scaling-factor 1.25

Now we can also test hibernation:

sudo systemctl hibernate

If you get Failed to hibernate system via logind: Sleep verb "hibernate" not supported, go into BIOS and disable secure boot (Enforce Secure Boot option). Unfortunately, the secure boot and hibernation still don’t work together but there is some work in progress to make it happen in the future. At this time, you need to select one or the other.

Assuming all works nicely, we can get firmware updates going:

fwupdmgr enable-remote -y lvfs-testing
fwupdmgr refresh
fwupdmgr update

And that’s it - just half a thousand steps and you have Ubuntu 24.04 with a ZFS mirror.

Manually Installing Encrypted ZFS on Ubuntu 24.04

For my daily driver I’m fortunate enough to have mirrored ZFS setup, my secondary machine has only a single SSD slot. While that makes mirror setup unproductive, I still want to use ZFS. Yes, it cannot automatically correct errors but it can at least help me know about them. And that’s before considering datasets, quotas, and beautiful snapshots.

For this setup I will use LUKS instead of the native ZFS encryption. Computer is fast enough that I don’t notice difference during daily work and total encryption is worth it for me.

As before, if you are beginner with ZFS, you might want to use Ubuntu’s built-in ZFS installatiion method. It will result in similar enough setup but without all these manual steps.

Why do I use this setup? Well, I like to setup my own partitions and I definitely love setting up my own datasets. In addition, this is the only way to make Ubuntu do a very minimal install. Lastly, I like to use hibernation with my computers and setting this during manual installation is often easier than sorting issues later.

With preamble out of the way, let’s go over all the steps to make it happen.

The first step is to boot into the USB installation and use “Try Ubuntu” option. Once on the a desktop, we want to open a terminal, and, since all further commands are going to need root access, we can start with that.

sudo -i

Next step should be setting up a few variables - disk, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK=/dev/disk/by-id/<whatever>
HOST=radagast
USERNAME=josip

I want to partition disk into 4 partitions. The first two partitions are unencrypted and in charge of booting (boot + EFI). While I love encryption, I almost never encrypt the boot partitions in order to make my life easier as you cannot seamlessly integrate the boot partition password prompt with the later password prompt. Thus encrypted boot would require you to type the password twice (or thrice if you decide to use native ZFS encryption on top of that). Third partition is largest and will contain all user data. The last partition is actually swap that we need for hibernation support. Since we’re using SSD, its position doesn’t matter (on the hard drive you definitely want it way ahead) so I put it these days at the end to match my mirrored ZFS setup.

All these requirements come in the following few partitioning commands:

DISK_LASTSECTOR=$(( `blockdev --getsz $DISK` / 2048 * 2048 - 2048 - 1 ))
DISK_SWAPSECTOR=$(( DISK_LASTSECTOR - 64 * 1024 * 1024 * 1024 / 512 ))

blkdiscard -f $DISK 2>/dev/null
sgdisk --zap-all                                $DISK
sgdisk -n1:1M:+255M           -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+1792M           -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:$DISK_SWAPSECTOR -t3:8309 -c3:LUKS $DISK
sgdisk -n4:0:$DISK_LASTSECTOR -t4:8200 -c4:Swap $DISK
sgdisk --print                                  $DISK

The next step is to setup all LUKS partitions. If you paid attention, that means we need to repeat formatting a total of 2 times. Unless you want to deal with multiple password prompts, make sure to use the same password for each:

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK-part3

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i $DISK-part4

Since creating encrypted partitions doesn’t mount them, we do need this as a separate step. I like to name my LUKS devices based on partition names so we can recognize them more easily:

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    $DISK-part3 ${DISK##*/}-part3

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    $DISK-part4 ${DISK##*/}-part4

Finally, we can set up our ZFS pool with an optional step of setting quota to roughly 85% of disk capacity. Since we’re using LUKS, there’s no need to setup any ZFS keys. Name of the pool will match name of the host and it will contain several datasets to start with. Most of my stuff goes to either Data dataset for general use or to VirtualBox dataset for virtual machines. Consider this just a suggestion and a good starting point, adjust as needed:

zpool create -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O quota=1600G \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    ${HOST^} /dev/mapper/${DISK##*/}-part3

zfs create -o canmount=noauto -o mountpoint=/ \
    -o reservation=100G \
    ${HOST^}/System
zfs mount ${HOST^}/System

zfs create -o canmount=noauto -o mountpoint=/home \
           ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home

zfs create -o canmount=noauto -o mountpoint=/Data \
           -o 28433:snapshot=360 \
           ${HOST^}/Data
zfs set canmount=on ${HOST^}/Data

zfs create -o canmount=noauto -o mountpoint=/VirtualBox \
           -o recordsize=32K \
           ${HOST^}/VirtualBox
zfs set canmount=on ${HOST^}/VirtualBox

zfs set devices=off ${HOST^}

With ZFS done, we might as well setup boot, EFI, and swap partitions too:

yes | mkfs.ext4 $DISK-part2
mkdir /mnt/install/boot
mount $DISK-part2 /mnt/install/boot/

mkfs.msdos -F 32 -n EFI -i 4d65646f $DISK-part1
mkdir /mnt/install/boot/efi
mount $DISK-part1 /mnt/install/boot/efi

mkswap /dev/mapper/${DISK##*/}-part4

At this time, I also often disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional:

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need to debootstrap our OS. As of this step, you must be connected to the Internet:

apt update
apt dist-upgrade --yes
apt install --yes debootstrap
debootstrap noble /mnt/install/

We can use our live system to update a few files on our new installation:

echo $HOST > /mnt/install/etc/hostname
sed "s/ubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
rm /mnt/install/etc/apt/sources.list
cp /etc/apt/sources.list.d/ubuntu.sources /mnt/install/etc/apt/sources.list.d/ubuntu.sources
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

At last, we’re ready to chroot into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env \
    DISK=$DISK HOST=$HOST USERNAME=$USERNAME USERID=$USERID \
    bash --login

With our newly installed system running, let’s not forget to set up locale and time zone:

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

dpkg-reconfigure tzdata

Now we’re ready to onboard the latest Linux kernel. I find that hwe is a nice compromise between using generic and oem but you can use whichever you or your hardware prefers:

apt update
apt install --yes --no-install-recommends linux-generic-hwe-24.04 linux-headers-generic-hwe-24.04

Now we set up crypttab so our encrypted partitions are decrypted on boot:

echo "${DISK##*/}-part3 $DISK-part3 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

echo "${DISK##*/}-part4 $DISK-part4 none \
      luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

cat /etc/crypttab

To mount all those partitions, we also need some fstab entries too. ZFS entries are not strictly needed. I just like to add them in order to hide our LUKS encrypted ZFS from the file manager:

echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value $DISK-part1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab

echo "/dev/mapper/${DISK##*/}-part3 \
    none auto nofail,nosuid,nodev,noauto 0 0" >> /etc/fstab

echo "/dev/mapper/${DISK##*/}-part4 \
    swap swap nofail 0 0" >> /etc/fstab

cat /etc/fstab

On systems with a lot of RAM, I like to adjust memory settings a bit. This is inconsequential in the grand scheme of things, but I like to do it anyway. Think of it as wearing “lucky” socks:

echo "vm.swappiness=10" >> /etc/sysctl.conf
echo "vm.min_free_kbytes=1048576" >> /etc/sysctl.conf

Now we can create the boot environment:

apt install --yes zfs-initramfs cryptsetup keyutils \
                  grub-efi-amd64-signed shim-signed

update-initramfs -c -k all

And then, we can get grub going. Do note we also set up booting from swap (needed for hibernation) here too. If you’re using secure boot, bootloaded-id HAS to be Ubuntu:

apt install --yes grub-efi-amd64-signed shim-signed
sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/${DISK##*/}-part4)\"/" \
    /etc/default/grub

update-grub

grub-install --target=x86_64-efi --efi-directory=/boot/efi \
             --bootloader-id=Ubuntu --recheck --no-floppy

I don’t like snap so I preemptively banish it from ever being installed:

apt remove --yes snapd 2>/dev/null
echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd
apt update

And now, finally, we can install our minimal desktop environment:

apt install --yes ubuntu-desktop-minimal man

Since Firefox is a snapd package (banished), we can install it manually:

add-apt-repository --yes ppa:mozillateam/ppa
cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF
apt update && apt install --yes firefox

Chrome aficionados, can install it too:

pushd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install ./google-chrome-stable_current_amd64.deb
popd

For hibernation, I like to change sleep settings so that hibernation kicks in after 13 minutes of sleep:

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/' \
    /etc/systemd/sleep.conf

For that we also need to do a minor lid switch configuration adjustment:

apt install -y pm-utils

sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/' \
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/' \
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' \
    /etc/systemd/logind.conf

Lastly, we need to have a user too.

adduser --disabled-password --gecos '' $USERNAME
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USERNAME
echo "$USERNAME ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
passwd $USERNAME

It took a while, but we can finally exit our debootstrap environment:

exit

Let’s clean all mounted partitions and get ZFS ready for next boot:

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

After reboot, we should be done and our new system should boot with a password prompt.

reboot

Our newly installed system should boot now.


[2024-08-22: Automatically determining the last sector in DISK_LASTSECTOR variable] [2024-08-28: Removed manual user ID assignment]