The unlock flow
How keyboot gets from a passphrase to an imported, mounted ZFS root. Extracted
from SPEC §5 (stages) and §7 (the keyfile model); the authoritative source is
SPEC + the code under init/ and tools/keyboot/src/.
The model in one paragraph
The operator’s passphrase does not key the data disks. It opens a small
LUKS1 keyfile container (a loopback file in keyboot’s initramfs); the
container’s 32-byte payload is the AES-256 key for every data disk. So one
passphrase → one payload → all disks. The payload (not the passphrase) is what’s
re-used after kexec so the booted OS unlocks without prompting again. The
decrypted payload is RAM-only, always — tmpfs, never non-volatile storage,
never the kernel cmdline, never dmesg//proc, zeroed after use (SPEC §17.1, the
critical invariants in CLAUDE.md).
passphrase ──▶ LUKS1 keyfile container ──▶ 32-byte payload ──┬──▶ open sn-<serial> (disk 1)
(multi-slot; slot opens ├──▶ open sn-<serial> (disk 2)
the same payload) └──▶ ... every data disk
│
zpool import (read-only)
│
pick BE ──▶ kexec BE kernel
│ (carry payload, ADR 0003)
BE initramfs: keyboot be-unlock
──▶ re-open LUKS ──▶ import rw ──▶ mount /sysroot
The keyfile container (SPEC §7)
- LUKS1 (not LUKS2), cipher
aes-xts-plain64, key-size 512, hashsha512, iter-time 5000ms — same params for the container and the data disks. - Multi-slot, each slot a passphrase that unlocks the same payload:
0 daily, 1 recovery, 2 automation (ADR 0013), 3–7 reserved. Adding
or changing a slot never disturbs the payload, so data-disk enrollment is
untouched (
keyfile passwd/rekey— see operator-guide.md). - The payload is generated once at install (
keyboot-install keyfile create) and each data disk is enrolled with it (keyboot-install keyfile enroll <disk>), so the disk’s LUKS key == the payload.rotateregenerates the payload and re-enrolls every disk.
Boot-time stages (the keyboot env)
Run by /init; each init/stage-N-*.sh is sourced in order (run_stage).
| Stage | What it does |
|---|---|
stage-1-early | mount /proc /sys /dev /run, udev coldplug, assemble the ESP md, mount /esp (ro), cap zfs_arc_max (ADR 0011) |
stage-2-net | bring up networking per the cmdline grammar (DHCP/static/VLAN/bond, matched by MAC) |
stage-3-ssh | start dropbear so the prompt + recovery are reachable over SSH (headless) |
stage-4-passphrase | obtain the passphrase (prompt on TTY/SSH, or a baked test passphrase in CI) |
stage-5-luks | open the keyfile container → then open every data disk as /dev/mapper/sn-<serial> using the payload |
stage-6-import | zpool import (read-only for a normal boot; read-write for keyboot.mode=provision) |
stage-7-discover | resolve which BE to boot: keyboot.be= > /esp/keyboot/once.next (consumed) > pool bootfs |
stage-8-menu | BE menu / actions (snapshot, rollback rungs, memtest); locate the BE’s kernel + initramfs |
stage-9-kexec | carry the payload as a RAM-only cpio overlay (ADR 0003) and kexec the BE’s own kernel |
The same Rust code drives the open path interactively: keyboot unlock <keyfile>
opens the container then every LUKS disk (--single <dev> for one; --dry-run
to report). keyboot keyfile info/list-slots inspect read-only.
Serial-based device naming
Data disks open as /dev/mapper/sn-<serial> so the pool topology survives bus
reordering (a disk that moves from sda to sdc keeps its sn- name). The
serial-discovery logic is a single source of truth in Rust
(tools/keyboot/src/disk/serial.rs), exposed as keyboot disk discover --json
and used by both the boot-time keyboot unlock and the install-time
orchestrator — install and boot agree on names by construction (no separate
shell discover script). On real hardware the serial comes from udevadm (the
static binary shells out) with a sysfs fallback.
Re-unlock after kexec (ADR 0003)
keyboot kexecs into the BE’s own kernel (the ZBM model), so the BE’s
initramfs must re-open the LUKS devices. keyboot hands the 32-byte payload
(not the passphrase) across the kexec, RAM-only, as a cpio overlay appended to
the BE’s initramfs; keyboot be-unlock consumes it (open keyfile→LUKS→import
rw→mount /sysroot), then both key copies are overwrite+unlink’d. Single prompt
for the whole boot.
# Inside the BE's initramfs, after kexec:
keyboot be-unlock --pool <P> --root-dataset <DS> [--keyfile F --root-mount /sysroot]
keyboot.handoff=0disables the carry and falls back to an independent BE re-unlock (dropbear-in-the-BE-initramfs, passphrase over SSH — ADR 0002). Slower (a second prompt) but carries no key across kexec; the CIqemu:install-os-handoff0gate covers it.- The payload must never touch non-volatile storage, the kexec cmdline,
dmesg/
/proc, or a crashdump. This is a do-not-break invariant (CLAUDE.md).
Where it lives
init/—/init+stage-1..9+init/lib/helpers.tools/keyboot/src/disk/serial.rs— disk/serial discovery (single source).tools/keyboot/src/cli/unlock.rs,…/be_unlock.rs— theunlock/be-unlockcommands.tools/keyboot/src/keyfile/— keyfile container create/enroll/open/slots.
See also: operator-guide.md (key rotation), disaster-recovery.md (when unlock fails), decisions/ (ADR 0003 handoff, 0013 automation slot).