1fa3250aa3
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.
127 lines
7.2 KiB
Markdown
127 lines
7.2 KiB
Markdown
# 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` (S1–S8). 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).
|