- CIS compliant disk layout — separate mount points for
/var,/tmp,/var/log,/var/tmp, and/var/log/audit, matching the CIS Benchmark's partitioning recommendations - CIS security settings are NOT included — no
nodev/nosuid/noexecmount-option hardening, no benchmark packages, no other CIS controls. This gives you the disk layout only; hardening is left for you to add separately
This documents an unattended (autoinstall) install of Ubuntu Server 26.04.1 with:
- ZFS root, using Subiquity's guided ZFS layout (the only ZFS layout exposed to autoinstall today; it is not offered in the interactive Server installer UI, only via autoinstall or the Desktop installer)
- Two extra datasets the guided layout doesn't create on its own (
var/tmp,var/log/audit), plus atmpdataset, added vialate-commands - Quotas on
var(8G) andtmp(6G) - A couple of quality-of-life tweaks (zsh as the login shell,
vim/zshpackages, a pinned 2G swap size)
It was built and tested in a Proxmox VM. Nothing here is Proxmox-specific except
the "build the VM" section — the user-data file itself works on any hypervisor
or bare metal that boots the Ubuntu Server 26.04.1 ISO.
zfs list / zpool list on a freshly installed system, showing the guided
layout plus the var/tmp, var/log/audit, and tmp datasets added by
late-commands:
The interactive Server installer (Subiquity's text UI) only offers LVM or a custom manual partition layout — there's no ZFS option in that menu, even though the Desktop installer has one. Subiquity's own maintainers have confirmed this directly: ZFS root on Server requires either a Desktop install, autoinstall, or a fully manual process. Autoinstall is the only one of those that stays server-oriented and scriptable.
- Ubuntu Server 26.04.1 ISO
- A way to build an ISO with
genisoimage(or equivalent) and attach it as a second virtual CD-ROM alongside the install media - Basic familiarity with
zfs/zpool
- Machine type:
q35, BIOS:OVMF(UEFI), with an EFI disk added - Leave Secure Boot off for simplicity (untested with Secure Boot + ZFS DKMS)
- Disk: SCSI, VirtIO SCSI single controller, Discard enabled, cache = "No cache"
- At least 4GB RAM (8GB+ more comfortable), 2 vCPUs is plenty for a test box
- 20-32GB disk is comfortable; ZFS datasets share pool space dynamically, so you
don't need to size
/var,/tmp, etc. individually the way you would with fixed partitions - Attach the Ubuntu Server ISO as one CD-ROM, and the autoinstall seed ISO (built below) as a second CD-ROM
None of this is required on other hypervisors — just make sure UEFI boot is available and you can attach a second CD-ROM/virtual media device.
Two files are needed: user-data (the actual autoinstall config) and an empty
meta-data (required by the NoCloud datasource format, even with minimal
content).
instance-id: ubuntu-zfs-test-01
local-hostname: ubuntu-zfs-test#cloud-config
autoinstall:
version: 1
locale: en_US.UTF-8
keyboard:
layout: us
network:
network:
version: 2
ethernets:
alleths:
match:
name: "en*"
dhcp4: true
# Guided ZFS root layout: creates bpool (/boot), rpool/ROOT/ubuntu_<id> (/),
# rpool/home (/home), and a swap partition. This is the only ZFS layout
# exposed to autoinstall today; extra datasets are added below.
storage:
layout:
name: zfs
swap:
size: 2G
identity:
hostname: ubuntu-zfs-test
username: youruser
# Generate with: openssl passwd -6
password: "$6$REPLACE_WITH_YOUR_HASH"
ssh:
install-server: true
allow-pw: true
packages:
- vim
- zsh
user-data:
chpasswd:
expire: false
late-commands:
# Set zsh as the login shell -- update the username to match identity.username above
- curtin in-target -- chsh -s /usr/bin/zsh youruser
# Ubuntu's guided ZFS layout already creates var and its usual zsys-style
# children (var/log, var/lib/apt, etc.) automatically. It does NOT create
# var/tmp, var/log/audit, or tmp -- those we create ourselves. Then apply
# quotas to var and tmp either way.
- |
curtin in-target -- bash -c '
set -euo pipefail
ROOTDS=$(zfs list -H -o name -t filesystem | grep -E "^rpool/ROOT/[^/]+$")
for sub in var/tmp var/log/audit tmp; do
ds="$ROOTDS/$sub"
if zfs list -H "$ds" >/dev/null 2>&1; then
continue
fi
path="/$sub"
stage=/mnt/zfs-stage
mkdir -p "$stage"
zfs create -o mountpoint="$stage" "$ds"
if [ -d "$path" ] && [ -n "$(ls -A "$path" 2>/dev/null)" ]; then
cp -a "$path"/. "$stage"/
fi
rm -rf "${path:?}"/*
zfs umount "$ds"
zfs set mountpoint="$path" "$ds"
done
zfs set quota=8G "$ROOTDS/var"
zfs set quota=6G "$ROOTDS/tmp"
'Before building the ISO, edit two things:
identity.usernameand the matching username in thechshlate-commandidentity.password— generate a real hash withopenssl passwd -6and paste it in (keep the quotes)
genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-dataThe volume label must be cidata — that's what the NoCloud datasource
looks for.
- Attach both the Ubuntu Server ISO and
seed.isoto the VM, boot from the Server ISO. - Recent Subiquity builds auto-detect the
cidatavolume and prompt to continue with autoinstall. If it doesn't, hiteat the GRUB menu and appendautoinstall ds=nocloud;to thelinuxline. - The install runs unattended and reboots when done.
zfs list # confirm var, var/tmp, var/log, var/log/audit, tmp, home all exist
zfs get quota rpool/ROOT/ubuntu_xxxxx/var rpool/ROOT/ubuntu_xxxxx/tmp
zpool status # pool health
cat /etc/cron.d/zfsutils-linux # confirm the built-in monthly scrub/TRIM cron jobzfsutils-linux (pulled in automatically as a dependency of ZFS root) ships a
cron job that TRIMs on the first Sunday of the month and scrubs on the second
Sunday, for every imported pool, with no extra configuration needed.
These are the non-obvious failures hit while building this, in case they save someone else the same round trips:
- The Server installer UI has no ZFS option at all, not even hidden in "Custom storage layout" — only Desktop, autoinstall, or a fully manual process support ZFS root on Server.
- Interface names aren't
eth0on modern kernels/VirtIO NICs — use amatch: name: "en*"pattern in the network config instead of a hardcoded name, or DHCP silently never applies and anything requiring network access during install (e.g. a package not in the ISO's offline pool) will fail. - Deleting/recreating a virtual disk doesn't guarantee a blank device on
LVM-backed storage (including LVM over iSCSI) — LVM by default only zeroes
the front of a new logical volume, not the whole thing. Leftover ZFS pool
labels from a previous attempt can survive
wipefs -aandsgdisk --zap-all(both only touch the partition table, not partition contents) and causezpool createto fail withis part of potentially active pool. The reliable fix is a full-disk zero:dd if=/dev/zero of=/dev/sda bs=1M status=progress. zpool get bootfs rpoolis not a reliable way to find the root dataset. Ubuntu's guided ZFS layout doesn't set thebootfsproperty, so this returns-. Usezfs list -H -o name -t filesystem | grep -E "^rpool/ROOT/[^/]+$"instead.- Watch your quoting if you nest a script inside
curtin in-target -- bash -c '...'inside alate-commandsentry — single quotes can't nest inside single quotes. Use double quotes for anything inside the outer single-quoted block (e.g. agrep -Epattern). - The guided ZFS layout already creates more separate datasets than you'd
expect:
varand its classic zsys-style children (var/lib,var/lib/apt,var/lib/dpkg,var/log,var/mail,var/snap,var/spool,var/www,var/games), plussrv,usr/local,home, androot. It does not createvar/tmp,var/log/audit, ortmp— those need to be added manually if you want them. zfs set mountpoint=<path> <dataset>already mounts the dataset as a side effect of the property change. A following explicitzfs mount <dataset>fails with "already mounted" — harmless in itself, but fatal underset -e. Don't callzfs mountright afterzfs set mountpoint=.- If an install fails, the installer drops you to a shell with the message
"An error occurred. Press enter to start a shell." From there,
curtinitself isn't onPATH(it lives inside subiquity's snap) — to poke around inside the partially-installed target, bind-mount and chroot manually:mount --bind /proc /target/proc mount --bind /sys /target/sys mount --bind /dev /target/dev chroot /target /bin/bash
- Different quotas or paths: edit the
QUOTAS-equivalent lines and thefor sub in ...list in the late-commands script. - Different packages: edit the
packages:list. - CIS mount-option hardening (
nodev/nosuid/noexec) is intentionally not included here — this only reproduces the CIS-style dataset/partition layout, not the security settings. Add ZFS dataset properties likesetuid=off/exec=off/devices=offper-dataset if you want that too.
