Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, hash sha512, 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. rotate regenerates 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).

StageWhat it does
stage-1-earlymount /proc /sys /dev /run, udev coldplug, assemble the ESP md, mount /esp (ro), cap zfs_arc_max (ADR 0011)
stage-2-netbring up networking per the cmdline grammar (DHCP/static/VLAN/bond, matched by MAC)
stage-3-sshstart dropbear so the prompt + recovery are reachable over SSH (headless)
stage-4-passphraseobtain the passphrase (prompt on TTY/SSH, or a baked test passphrase in CI)
stage-5-luksopen the keyfile container → then open every data disk as /dev/mapper/sn-<serial> using the payload
stage-6-importzpool import (read-only for a normal boot; read-write for keyboot.mode=provision)
stage-7-discoverresolve which BE to boot: keyboot.be= > /esp/keyboot/once.next (consumed) > pool bootfs
stage-8-menuBE menu / actions (snapshot, rollback rungs, memtest); locate the BE’s kernel + initramfs
stage-9-kexeccarry 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=0 disables 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 CI qemu:install-os-handoff0 gate 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 — the unlock / be-unlock commands.
  • 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).