Files
felhom.eu/scripts/iso/README.md
T
admin 1fa3250aa3 feat(iso): R-21 slice A — bare-metal Felhom ISO pipeline + first-boot bootstrap
scripts/iso/: a DooPlex pipeline (build-felhom-iso.sh + Dockerfile.assistant) that
turns the official PVE ISO into a Felhom auto-install ISO whose first-boot stub
installs a retry-forever felhom-bootstrap unit which unattended-fetches
felhom-host-install.sh from the public felhom.eu/scripts channel and runs it until the
host is enrolled + a guest provisioned. host-install is UNMODIFIED (invoked only).

- build gates the answer on validate-answer OUTPUT text, never $? (spike S1 exit-0 trap)
- stub is from-iso, fully-up, exactly-once; retry unit owns all network work (S8a)
- retry-vs-resume encoded once: plain first, --resume when install state exists (v1.11.3)
- secret-bearing (embeds the retrieval passphrase): supervised/single-use; env shredded on success

Validated on VM 310: build gate + red-proof, disk-filter fail-safe, chain + retry,
resume-decision, exactly-once, no-net retry+recovery. Terminal host-install rc-0 success
operator-gated (drill customer needs the password-gated create-UI). scripts v1.16.0;
ROADMAP R-21 -> in-progress. Detail in REPORT.md.
2026-07-16 13:47:08 +02:00

127 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Felhom bare-metal ISO pipeline (R-21 slice A)
Turns the official Proxmox VE ISO into a **Felhom auto-install ISO** whose first-boot chain,
unattended, fetches and runs `felhom-host-install.sh` until the host is enrolled and a guest is
provisioned. Boot the ISO on blank hardware → walk away → managed Felhom host.
Grounding: every install/first-boot/webhook mechanism used here is proven in
`documentation/audits/SPIKE-baremetal-iso-2026-07-16.md` (S1S8). This pipeline **composes** them;
it does **not** modify `felhom-host-install.sh` (which it only invokes over the public distribution
channel).
> **Scope:** slice A = the pipeline + the in-ISO bootstrap, validated on nested VM 310. The physical
> N100 run is **RUNBOOK B** (separate, supervised). The customer-facing claim/pairing that removes the
> secret from the ISO is **slice C** (not built here).
## Files
| File | Role |
|---|---|
| `Dockerfile.assistant` | build host: Debian trixie + `proxmox-auto-install-assistant` (paired to the ISO's PVE generation by Debian codename) + `xorriso` + `shellcheck` |
| `build-felhom-iso.sh` | the pipeline: render answer, mint throwaway root hash, **validate-answer output-parse gate**, render stub, `prepare-iso`, emit ISO + sha256 + manifest |
| `answer.toml.tmpl` | Proxmox answer template (`__FQDN__` / `__ROOT_HASH__` / `__ROOT_SSH_KEYS__` / `__DISK_SETUP__` placeholders; `[first-boot]` from-iso/fully-up) |
| `profiles/*.profile` | disk-selection + fqdn fragments (sourceable, no secret) |
| `stub-first-boot.sh` | the ONE first-boot executable (skeleton; build injects the bootstrap script/unit/env as base64) |
| `felhom-bootstrap.sh` | per-attempt: fetch host-install from the public channel → run it with the retrieval passphrase → on rc 0 write done-flag + disable |
| `felhom-bootstrap.service` | retry-forever unit (`Type=oneshot`, `Restart=on-failure`, `RestartSec=30`, `StartLimitIntervalSec=0`) |
## Build
```bash
# 1. build the assistant image once (rebuild when the target PVE generation changes)
docker build -f scripts/iso/Dockerfile.assistant -t felhom-iso-assistant:trixie scripts/iso
# 2. build an ISO (on the build host; PVE ISO pre-downloaded + its official sha256)
scripts/iso/build-felhom-iso.sh \
--pve-iso /path/proxmox-ve_9.2-1.iso \
--iso-sha256 4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c \
--profile scripts/iso/profiles/nested-vm.profile \
--bootstrap-env /secure/bootstrap.env \
--out ~/felhom-iso/out
```
Output: `felhom-pve-<pvever>-v<isover>-<profile>.iso` + `.sha256` + `.manifest.txt`.
The build **gates the answer on validate-answer's OUTPUT TEXT, never `$?`**`validate-answer`
returns exit 0 even on a broken file (spike S1 trap; also `prepare-iso` was observed to exit 0 on a
bad answer). A broken answer aborts the build with the validator's message and produces **no ISO**.
Note `validate-answer` is *syntactic only*: disk existence is a runtime property (a non-existent disk
passes the build and fails-safe at install — spike S5c/S8b).
## Profile format
A sourceable shell fragment:
```sh
FELHOM_FQDN="felhom-host.local"
FELHOM_DISK_SETUP='[disk-setup]
filesystem = "ext4"
disk-list = ["sda"]'
# optional emergency/validation key baked into root's authorized_keys (blank -> not baked):
# FELHOM_ROOT_SSH_KEY="ssh-ed25519 AAAA... ops@felhom"
```
`FELHOM_DISK_SETUP` is any valid Proxmox `[disk-setup]` block — a `disk-list` OR a udev `filter.*`
glob. **A filter matching nothing, or a non-existent disk, fails-safe**: the installer aborts (exit
1, no disk touched) rather than installing on the wrong disk (spike S5c). Never commit a real key in
`FELHOM_ROOT_SSH_KEY` unless it is meant to ship in every ISO built from that profile.
## bootstrap-env (SECRET-BEARING)
The `--bootstrap-env` file becomes the in-ISO `/etc/felhom/bootstrap.env`:
```sh
FELHOM_CUSTOMER_ID=<hub customer id> # required
FELHOM_MODE=appliance # required (appliance|byo)
FELHOM_RETRIEVAL_PASSPHRASE=<customer passphrase> # required — SECRET
# optional:
# FELHOM_HUB_URL=https://hub.felhom.eu
# FELHOM_INSTALL_URL=https://felhom.eu/scripts/felhom-host-install.sh
# FELHOM_EXTRA_ARGS="--cores 4 --memory 8192" # profile-only flags, never secrets
```
**Why the ISO is secret-bearing (§4.4 finding):** the hub install-command's *distribution channel* is
the public `https://felhom.eu/scripts/felhom-host-install.sh` — no token, no secret in the command.
But `felhom-host-install.sh` requires the customer **retrieval passphrase** (normally a no-echo
prompt) to fetch the config and enroll the host. An *unattended* install must supply it via
`--passphrase-file`, so the ISO embeds it. **Rules for a secret-bearing ISO:**
- supervised / single-use only; **never distributed**; **delete after the run**.
- issue the passphrase with the shortest usable lifetime; rotate/retire after the install.
- the bootstrap `shred`s `bootstrap.env` on the box once host-install succeeds (reduces secret-at-rest).
- **slice C** removes this: the claim/pairing flow delivers the passphrase (or config) to the box
*after* the customer claims it, so the *distributed* ISO carries no secret.
## The first-boot chain
`stub-first-boot.sh` (exactly-once, `[first-boot]` from-iso/fully-up) is **dumb**: it lays down
`/usr/local/sbin/felhom-bootstrap.sh`, the systemd unit, and `/etc/felhom/bootstrap.env` (0600), then
enables + starts the unit. All fallible/network work lives in `felhom-bootstrap.service`, which
retries forever until `felhom-host-install.sh` exits 0, then writes `/etc/felhom/.bootstrap-done`,
disables itself, and scrubs the env. Retry-vs-resume: the first attempt is plain; any later attempt
that finds `/var/lib/felhom-install/state.json` adds `--resume` (safe per host-install v1.11.3 — its
producer steps re-run each pass).
## N100 profile (for RUNBOOK B)
The nested profile uses `disk-list=["sda"]`. **A real single-disk mini-PC should use a unique-serial
udev filter, not `disk-list`:**
1. Boot once (or use a live env), harvest the target disk's udev id:
`udevadm info --query=property --name=/dev/sda | grep -E 'ID_SERIAL_SHORT|ID_MODEL|ID_WWN'`.
2. Write a profile: `filter.ID_SERIAL_SHORT = "<harvested>"` + `filter-match = "all"`. A
match-nothing filter fails-safe (won't wipe the wrong disk).
3. **Prior-LVM prerequisite:** the auto-installer does **not** force-clear a pre-existing LVM PV
signature — a disk with a previous Linux/Proxmox install aborts at "cannot init physical volume"
(spike S2b). Wipe first: `blkdiscard -f /dev/<disk>` (or `wipefs -a` + zero the PV region).
4. UEFI + Secure Boot: no caveat — installs and boots under SB enforcing, no MOK/keypress (spike S2b).
## Validation status (nested VM 310, 2026-07-16)
PASS: build gate + red-proof (C); disk-filter fail-safe through the pipeline (D); stub→unit→**real
public-channel fetch**→correct host-install invocation→retry; 2.3 resume decision; exactly-once stub;
no-network retry + auto-recovery (B). **Operator-gated (not live-proven here):** host-install rc-0
terminal success → hub-enrolled + guest-provisioned + done-flag + unit-disable — needs a properly
created drill customer (the hub create-UI is password-gated and a live-DB insert is unsafe). Also
out of scope: the physical N100 run (RUNBOOK B).