2018-01-14

Installing ZFS on Devuan (Again)

We've already made a systemd-free Linux server with ZFS-on-root, so let's have some fun. Typically I like to build a system with the tools of that system: use a Mint LiveCD to install Mint, Ubuntu to install Ubuntu, and so on, but it's not strictly necessary.

In particular, you can use an Ubuntu 16.04-based LiveCD with built-in ZFS support to save yourself from having to compile ZFS kernel modules twice. At the end of the day, your machine will have Devuan on it, even if you entirely installed and configured it from inside an Ubuntu or Mint session.

Here's a remix of how to build a Devuan system using a Linux Mint 18.x LiveCD and a decent network connection. Unlike the previous howto, this one will combine the stable and unstable Devuan package repositories. The advantage of doing this is a system with a modern Linux kernel and a newer version of the ZFS modules.

This is slightly more advanced than the previous howto, but don't worry. You should be a pro at this by now, and we're not going to be doing anything too terribly different here in terms of the core concepts you've already mastered. We'll be making a new LUKS container, we'll be putting ZFS on the container, installing an OS onto ZFS, and finally configuring the bootloader to decrypt and mount it. Easy peasy.

First, fetch the Linux Mint 18.x ISO and boot your machine. I like using linuxmint-18.3-xfce-64bit.iso, but use what you like. Start a terminal and become root and install the two packages you need to continue:

sudo su
killall light-locker # no screens shall be saved
apt update
apt install -y zfsutils-linux debootstrap

Partition your disk and create a LUKS container for it. This howto assumes your disk is /dev/sda and you're putting one partition on it. Your actual mileage may vary.

CRYPTNAME=cryptroot
DEVICE=/dev/sda
PARTITIONNUMBER=1
PART=${DEVICE}${PARTITIONNUMBER}

wipefs --force --all ${DEVICE}
dd if=/dev/zero of=${DEVICE} bs=1M count=2

/sbin/parted --script --align opt ${DEVICE} mklabel msdos
/sbin/parted --script --align opt ${DEVICE} mkpart pri 1MiB 100%
/sbin/parted --script --align opt ${DEVICE} set ${PARTITIONNUMBER} boot on
/sbin/parted --script --align opt ${DEVICE} p

cryptsetup luksFormat -h sha512 ${PART}
cryptsetup luksOpen ${PART} ${CRYPTNAME}

Check that you have a /dev/mapper/cryptroot LUKS container and note its UUID value:

cryptsetup luksDump ${PART}
blkid -o export ${PART} | grep -E '^UUID='

Create your zpool.

ZPOOLNAME=zroot
ZROOTDATASETNAME=jessie
CRYPTNAME=cryptroot
VDEV=/dev/mapper/${CRYPTNAME}
TARGET=/mnt

# /sbin/modprobe zfs # skip this if ZFS is not a kernel module (Ubuntu 16.04+)
/sbin/zpool create -f \
  -R ${TARGET} \
  -O mountpoint=none \
  -O atime=off \
  -O compression=lz4 \
  -O normalization=formD \
  -o ashift=12 \
  ${ZPOOLNAME} ${VDEV}

/sbin/zfs create -o canmount=off        ${ZPOOLNAME}/root
/sbin/zfs create -o mountpoint=/        ${ZPOOLNAME}/root/${ZROOTDATASETNAME}
/sbin/zfs create -o mountpoint=/boot    ${ZPOOLNAME}/boot
/sbin/zfs create -o mountpoint=/home    ${ZPOOLNAME}/home
/sbin/zfs create -o mountpoint=/var     ${ZPOOLNAME}/var
/sbin/zfs create -o mountpoint=/var/log ${ZPOOLNAME}/var/log

/sbin/zpool set bootfs=${ZPOOLNAME}/root/${ZROOTDATASETNAME} ${ZPOOLNAME}

Install your OS. We use debootstrap here, and we leverage a couple of bonus packages we want with --include. You could really go overboard here and install the kitchen sink. I prefer to keep --include lean (but not too lean) and add what I need later.

ARCH=amd64
BRANCH=jessie
TARGET=/mnt
# HTTPS works here but not for apt
MIRROR=https://auto.mirror.devuan.org/merged
PKGS=console-setup,cryptsetup,kbd,locales,tmux,openssh-client

/usr/sbin/debootstrap \
  --arch=${ARCH} \
  --include=${PKGS} \
  ${BRANCH} \
  ${TARGET} \
  ${MIRROR}

N.B.: I have not had much luck with a reliable way to intelligently install packages with respect to their dependencies other than debootstrap and the apt family of tools. apt, apt-get, aptitude, and their ilk expect a network connection to find and fetch packages. This makes, say, downloading a set of .DEB files to a local file share and then saying "go install these before I turn your network interface on" a problem. I've done experimentation with a number of tools that are ultimately unsatisfying: multistrap and gdebi come to mind. If you only want to install Devuan once, go and make your bespoke debootstrap --include as long as you want. I like to keep the fetched packages around, un-bootstrapped in a tarball, to help me create reproducibly similar systems. You can do this with the --foreign argument, with the cost of needing to chroot to the new system-to-be and run debootstrap, locally, a second time. Doing so is outside the scope of this howto, but it can be done.

When the base system has installed successfully, put your new fstab in place. For example:

# cat /mnt/etc/fstab
/dev/mapper/cryptroot /        zfs defaults,noatime 0 0
zroot/boot            /boot    zfs defaults,noatime 0 0
zroot/home            /home    zfs defaults,noatime 0 0
zroot/var             /var     zfs defaults,noatime 0 0
zroot/var/log         /var/log zfs defaults,noatime 0 0

Other important files that need to be updated:

echo myhostname > /mnt/etc/hostname
echo en_US.UTF-8 UTF-8 > /mnt/etc/locale.gen
echo 127.0.0.1 myhostname >> /mnt/etc/hosts
ln -sf /proc/self/mounts /mnt/etc/mtab

Set your network config. This howto assumes DHCP for simplicity. Linux networking is terrible, so I chattr the interfaces file to keep it from being molested by something well-meaning but misguided that wants to make sure my minimalist server OS can connect to a coffeeshop wifi access point if one appears, because apparently there could someday be a Starbucks that opens up inside a datacenter. I do this to /etc/resolv.conf, too.

echo auto eth0 >> /mnt/etc/network/interfaces
echo iface eth0 inet dhcp >> /mnt/etc/network/interfaces
chattr +i /mnt/etc/network/interfaces

Add some mountpoints into your /mnt:

for i in /dev /dev/pts /proc /sys; do mount -B $i /mnt$i; done

Create a key for your bootloader to use to unlock the LUKS container.

TARGET=/mnt
KEYDIR=${TARGET}/boot
DEVICE=/dev/sda
PARTITIONNUMBER=1
PART=${DEVICE}${PARTITIONNUMBER}
INITRAMFSHOOKSDIR=${TARGET}/etc/initramfs-tools/hooks
KEYFILE=rootkey.bin

openssl rand -out ${KEYDIR}/${KEYFILE} 2048 # you can always use dd here too
chmod 0 ${KEYDIR}/${KEYFILE}
cryptsetup luksAddKey ${PART} ${KEYDIR}/${KEYFILE}

mkdir -p ${INITRAMFSHOOKSDIR}
echo "cp -p /boot/${KEYFILE} \"\${DESTDIR}\"" > ${INITRAMFSHOOKSDIR}/crypto_keyfile
chmod +x ${INITRAMFSHOOKSDIR}/crypto_keyfile

chroot into your system and configure it. tmux or GNU screen would be useful here, hence why I put tmux in the debootstrap. Some of the following steps can be done while you're waiting for things to compile.

chroot /mnt

Set up your apt repos. Make sure you have the Devuan unstable branch, "ceres", and it should include at least the "main" and "contrib" categories. Debian refugees may recall that their unstable branch is "sid", so there may be a period of adjustment re-learning that sid is now ceres.

cd /etc/apt
cp -p sources.list sources.list.orig
vi sources.list # do your editing here
# cat sources.list
deb http://auto.mirror.devuan.org/merged jessie main
deb http://auto.mirror.devuan.org/merged ceres main contrib

Get the Devuan repo key. It is absent from your new system because you didn't use a Devuan installation medium. You could get this key from the Devuan LiveCD in /usr/share/keyrings, but I'll show you how to do it by hand here:

gpg --verbose --keyserver=pgp.mit.edu --recv-key 94532124541922FB
gpg --verbose --export --armor --output=./devuan_jessie.key 94532124541922FB
apt-key add ./devuan_jessie.key

Now you can begin to customize your packages. At minimum, we'll be installing a compiler, a kernel, a bootloader, and some kernel modules. If your architecture isn't AMD64, adjust it accordingly.

apt update
apt install -y build-essential
apt install -y -t ceres linux-image-amd64
apt install -y -t ceres linux-headers-amd64

I find that when mixing stable and unstable repos, meaning both jessie and ceres, the newer package usually wins. If that's the case, then the -t ceres argument isn't strictly necessary. I like to include it anyway for clarity. I want the latest kernel and kernel headers the repo has to offer.

Install ZFS. This can take a while.

DEBIAN_FRONTEND=noninteractive
export DEBIAN_FRONTEND
time apt install -y -t ceres zfs-dkms

Create a password for root. Pick a time zone and a locale. I typically like to do this in another tmux window while ZFS builds.

passwd
passwd -u root
dpkg-reconfigure tzdata
locale-gen

When ZFS finishes installing, continue by adding a ZFS-aware initramfs

apt install -y -t ceres zfs-initramfs grub-pc

Configure GRUB and prep your initramfs. The UUID value you created at the beginning will be important here. As an example, this howto assumes your UUID is 9862499a-80b0-459d-9a86-5f2ddbe0464c. Replace this value with your real UUID. If your LUKS container is named something other than cryptroot, adjust that, too.

blkid -o export /dev/sda1 | grep -E '^UUID='
vi /etc/crypttab
cat /etc/crypttab
cryptroot UUID=9862499a-80b0-459d-9a86-5f2ddbe0464c /rootkey.bin luks,keyscript=/bin/cat

Make sure /etc/default/grub contains the following:

GRUB_CMDLINE_LINUX_DEFAULT="boot=zfs"
GRUB_CMDLINE_LINUX="cryptdevice=UUID=9862499a-80b0-459d-9a86-5f2ddbe0464c:cryptroot"
GRUB_ENABLE_CRYPTODISK=y

Test if GRUB can detect your ZFS dataset.

grub-probe /

If the result isn't "zfs", something is wrong. Do not continue until the problem is fixed.

Create a new initramfs. Recent updates may have precluded the need to symlink /dev/mapper/cryptroot to /dev/cryptroot; your actual mileage may vary.

ln -sf /dev/mapper/cryptroot /dev
update-initramfs -u -k all

Update GRUB and install the GRUB bootloader.

update-grub
grub-install /dev/sda

If the result is "Installation finished. No error reported." you can proceed.

Disable log compression for /var/log. Since you're already using lz4 compression on the zpool, further per-file compression is, in general, unhelpful.

for file in /etc/logrotate.d/* ; do
  if grep -Eq "(^|[^#y])compress" "$file" ; then
    sed -i -r "s/(^|[^#y])(compress)/\1#\2/" "$file"
  fi
done

When the system is configured how you want it, exit the chroot.

exit

Unmount your mountpoints, change your non-root datasets' mountpoint to "legacy", and export the zpool.

for i in sys proc dev/pts dev
do
  umount /mnt/$i
done

/sbin/zfs unmount -a

for dataset in boot home var/log var
do
  /sbin/zfs set mountpoint=legacy zroot/${dataset}
done

/sbin/zpool export -a

Finally, stop the machine, eject the LiveCD, and boot off of the disk. You will be prompted to unlock the LUKS container with a password, and from there the boot process should continue without further prompting.

Login as root. Add a swap device zvol.

ZPOOLNAME=zroot
SWAPNAME=swap
DEVICENAME=/dev/zvol/${ZPOOLNAME}/${SWAPNAME}

/sbin/zfs create -V 128M -b $(getconf PAGESIZE) \
  -o compression=zle \
  -o logbias=throughput \
  -o sync=always \
  -o primarycache=metadata \
  -o secondarycache=none \
  -o com.sun:auto-snapshot=false \
${ZPOOLNAME}/${SWAPNAME}

mkswap -f ${DEVICENAME}
echo ${DEVICENAME} none swap defaults 0 0 >> /etc/fstab
swapon -av

If you ever find yourself adding another kernel to the system, linux-image-A.B.C-D-amd64 for example, make sure you add linux-headers-A.B.C-D-amd64 as well. Adding the corresponding kernel headers should, in theory, run /etc/kernel/header_postinst.d/dkms and build the necessary ZFS kernel modules for the new kernel automatically. Always be cautious when and how you update your kernel.

No comments: