0008 — Packaging & delivery (curl|sh first, then native repos)
Status: accepted (2026-06-22) — ratified by maintainer; proposed 2026-06-04
Context
SPEC §3/§10 commit keyboot to “CI-signed artifacts shipped as native packages
(apk, ebuild, deb) in our own repos; ansible manages the repo definitions,” with
a signed curl | sh bootstrap (§13.10) and a host at packages.osterman.co. None of
that exists yet, and two new userland tools (keyboot-autosnap,
keyboot-be-upgrade) plus the boot image now need a distribution story. This ADR
fixes the artifact set, the package split, the channels, and the staging
order so we can build the lowest-friction path first without re-architecting
when the native repos land.
Decision
Artifact set & package split
Three packages from the build, split so the dangerous/heavy bits are isolated:
keyboot— runtimekeybootbinary +keyboot-autosnap+keyboot-be-upgrade+ thegrub.d/10_keybootseam. Installs on the running host. (be-upgradeis host-safe: it clones, never wipes.)keyboot-boot— the kernel + initramfs (large, per-kernel). Its post-install stages to/var/lib/keyboot/staged/first, then — per the superseding ADR 0009 (A/B ESP slots) — auto-promotes: writes the new keyboot into the inactive A/B slot and arms a boot-once trial, so a failure auto-reverts to the known-good slot. This answers the §10.3/§16.3 “package scripts touching/bootmake operators nervous” without leaving the commit manual: the boot-counter is the safety net the original “stage-and-operator- commits” stance lacked.KEYBOOT_AUTOPROMOTE=0opts back into staging-only (the operator then commits withkeyboot install). ADR 0009 closes ADR 0004’s keyboot-self-upgrade boundary.keyboot-install— rescue/install only; never auto-installed on a running host (SPEC §3 binary split).
The two shell tools are arch-independent; keyboot/keyboot-install are
x86_64-musl static (aarch64 later). Shell stays shell deliberately
(zero-dep, runs in recovery, easy to audit); Rust stays for crypto/FFI.
Channels & staging order
Phase A — packages.osterman.co static host + signed curl | sh (build first).
A small static site (nginx container behind the existing proxy; new
/root/layout entry + proxy route) that CI publish rsyncs into:
packages.osterman.co/keyboot/<ver>/{keyboot-x86_64-musl, keyboot-install-x86_64-musl,
keyboot-autosnap, keyboot-be-upgrade, keyboot-vmlinuz, keyboot-initramfs.img,
manifest.json, SHA256SUMS, SHA256SUMS.minisig}
packages.osterman.co/keyboot/install # the bootstrap (trailing minisig)
packages.osterman.co/keyboot/latest # version pointer
- Signing: minisign for this channel (one keypair, trailing-block-friendly, tiny verifier). Pubkey baked into the keyboot image + documented for pre-fetch. The bootstrap verifies before exec / before install.
ci/install.shis the bootstrap: detect arch/distro → fetchlatest(or a pinnedKEYBOOT_VERSION) → verify SHA256SUMS.minisig → installkeyboot-autosnap+cron,keyboot-be-upgrade, and thekeybootbinary;--rescuealso pullskeyboot-install+ the boot image. Idempotent.
Phase B — native repos (§10.3 end state).
- Alpine
abuild→ signed.apk+APKINDEXatpackages.osterman.co/alpine/ <branch>/; pubkey to/etc/apk/keys/. install-os can pre-add the repo so BEs auto-update. - Debian
.deb→ reprepro/aptly (InReleaseGPG) at…/debian/<codename>/. - Gentoo ebuild overlay git repo
git.osterman.co/west17m/keyboot-overlay(+ Manifest GPG; optional binhost). - Ansible role adds the repo def + key per host.
Signing identities
minisign for curl|sh; native repos use their own (apk RSA, apt GPG, gentoo Manifest GPG) — all keys in GitLab CI variables (§10.2). Open: unify GPG across apt+gentoo.
Version ↔ ZFS-floor coupling
keyboot/keyboot-boot package versions encode the OpenZFS feature floor
(ADR 0004), so the package manager can refuse a pool-feature bump until keyboot
is upgraded. manifest.json carries the zfs version + compat baseline.
Alternatives considered
- Native repos first: higher friction to stand up (per-distro signing + index tooling) before anything is installable. curl|sh from a static host is the faster MVP and serves every distro at once; native repos layer on.
- GitLab Package Registry only: fine for generic binary pulls, awkward for the predictable repo-index paths apk/apt expect. Use a static host.
- Rewrite the shell tools in Rust for one package format: loses the audit/recovery-friendliness; the tools must run in minimal envs. Rejected.
- Auto-commit the ESP on package upgrade: originally rejected as the
operator-nervous footgun (a bad write bricks the boot stage). ADR 0009
revisits this: with A/B ESP slots + a boot-once trial that auto-reverts, the
staged image can be auto-promoted safely — the failure mode the footgun
worried about is now caught by the boot-counter.
KEYBOOT_AUTOPROMOTE=0keeps the manual stage-then-commit for the conservative operator.
Consequences
- New infra:
packages.osterman.co(static + minisign) must be stood up — a/root/layoutentry + proxy route + a CIpublishstage. Until then,ci/install.shcan point at aKEYBOOT_PKG_BASEoverride (e.g. a GitLab artifact URL) so the bootstrap is testable before the host exists. - The build pipeline (§10.1) gains
package(apk/deb/ebuild + tarballs) andpublishstages aftersign. - ESP integrity:
keyboot-boot’s post-install is now governed by ADR 0009 (A/B ESP slots, auto-promote with boot-once auto-revert) — no longer an open boundary. install-osalready installs the BE tools directly (build-image staging); the native packages are the update path for already-running hosts.