Files
felhom.eu/scripts/iso/README.md
T
admin 317037f8eb scripts v1.22.0 — ISO boot screen + single-entry GRUB menu (R-38 GRUB slice)
Two jobs, one repack pass.

BRANDING. Every ISO now carries a Felhom boot screen built from the website's
og-image_2.png at repack time (ImageMagick in the assistant container), so the
boot card has ONE source and not a second pre-rendered copy in the repo to
drift. The card is scaled onto a 1024x768 gfxterm canvas, top-centered, and the
card's own subtle background grid is continued across the letterbox fill
PHASE-LOCKED to where the card's grid lands — the fill is seamless rather than a
square of grid floating in flat navy. Menu positioning needs a gfxmenu theme
(plain background_image cannot move the menu off the wordmark), so the stock
pvetheme is replaced by felhomtheme, which puts the menu in the lower third the
layout deliberately leaves empty.

SAFETY — the half that matters. The stock PVE menu offers Graphical, Terminal
UI and serial installers plus an Advanced Options submenu (nomodeset x2, three
debug variants, Rescue Boot, memtest, UEFI settings). Every one of them reaches
the MANUAL installer, whose first question is which disk to wipe. A customer, or
their helpful nephew, must not be able to get there from a boot menu. They are
not hidden and not password-gated: they are NOT EMITTED. What ships is one
entry, 'Felhom telepítés', default, 5s.

Boot behavior is unchanged. The kernel/append and initrd lines are lifted
VERBATIM from the ISO's own 'Install Proxmox VE (Automated)' entry rather than
frozen into a copy here, so a PVE bump tracks automatically; the build fails if
they cannot be found, if the append line has lost proxmox-start-auto-installer,
or if auto-installer-mode.toml is absent (which would mean the one Felhom-
labelled entry boots a manual installer). The rendered menu is then gated for
exactly 1 entry, 0 submenus, and zero references to proxtui/proxdebug/nomodeset/
Rescue Boot/memtest/fwsetup — and re-verified by reading the menu back OUT of
the finished ISO, not merely out of the extract tree.

mkimage-surgery.sh -> iso-repack.sh: branding and the slice-B loader swap need
the same extract -> modify -> re-master cycle, so they share one pass instead of
re-mastering twice. The mkimage recipe is untouched. The embedded module list is
still derived from the STOCK grub.cfg (snapshotted before branding rewrites it),
plus gfxmenu's bitmap/bitmap_scale/trig renderer deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 08:45:45 +02:00

12 KiB
Raw Blame History

Felhom bare-metal ISO pipeline (R-21 slice A + B)

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, optional --loader mkimage surgery, emit ISO + sha256 + manifest
iso-repack.sh the post-prepare-iso repack (runs in the container). v1.22.0/R-38: Felhom GRUB branding + single-entry menu surgery; plus, in --loader mkimage, the slice-B monolithic grub-mkimage UEFI loader swap (F1 firmware fix). One extract→modify→re-master pass for both. (was mkimage-surgery.sh ≤ v1.21.0)
grub/grub.cfg.tmpl the replacement GRUB menu: exactly one entry („Felhom telepítés", default, 5 s). The linux/initrd lines are lifted verbatim at repack time from the ISO's own Install Proxmox VE (Automated) entry, so a PVE bump tracks automatically
grub/felhom-theme.txt gfxmenu theme — positions the menu in the clean lower third of the boot card (plain background_image cannot position the menu, so the text would land on the wordmark)
grub/generate-grub-background.sh letterboxes website/assets/og-image_2.png onto a 1024×768 gfxterm canvas, phase-locking a continuation of the card's own background grid across the fill
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, TimeoutStartSec=infinity — v1.21.0/R-33: the PAIRING wait loops inside the script, so the unit sits in activating instead of failing every 30s; without the lifted timeout systemd would kill it at 90s and restore the console spam)

Build

# 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       /mnt/5_hdd/felhom.eu/felhom-iso/out

On the DooPlex build server (180) the felhom working dirs were moved off the SSD to /mnt/5_hdd/felhom.eu/ (2026-07-18): the input PVE ISO lives at /mnt/5_hdd/felhom.eu/drill/proxmox-ve_9.2-1.iso. The script's --out default is now /mnt/5_hdd/felhom.eu/felhom-iso/out (override with $FELHOM_ISO_OUT on other hosts), so on 180 you can omit --out entirely and output still lands on the HDD, never the SSD.

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:

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.

A profile may also set FELHOM_LOADER="shim"|"mkimage" (see below); --loader on the command line overrides it.

Loader modes (--loader shim|mkimage, slice B)

The UEFI boot loader is selectable. Default shim; a profile can pin FELHOM_LOADER; --loader wins over both.

  • shim (default) — the stock output: the ISO's MS-signed shim → GRUB chain. Secure Boot keeps working on compliant firmware (no MOK, no keypress — spike S2b). Use this everywhere it boots.

  • mkimage — replaces the ISO's UEFI boot path with a monolithic grub-mkimage loader built from the ISO's own GRUB modules. This is the fix for cheap AMI (AN3PLUS 0.01-class) mini-PC firmware that cannot UEFI-boot the ISO's stock GRUB from USB (relocation 0x0 is not implemented yet; SB-off and shim-bypass don't help) — the F1 finding of tests/VALIDATION-n100-baremetal-2026-07-16.md, worked around live during that run and now first-class. The recipe (module set from the ISO's grub.cfg, embedded search --fs-uuidconfigfile the real menu) is reproduced from the run evidence, not re-derived.

    ⚠ mkimage is UNSIGNED → the target board MUST have Secure Boot OFF. With Secure Boot enforcing the firmware refuses the loader (Access Denied, no boot — proven, RUNBOOK-B leg 3). mkimage ISOs are named with a -mkimage suffix and the manifest's loader: line says so; never confuse them with a shim (SB-capable) build. shim mode's output is byte-for-byte the same as before this flag existed (the surgery is skipped entirely).

    The surgery runs after prepare-iso and touches only the EFI boot path — the answer file and the on-first-boot payload are provably byte-identical to the shim variant's (RUNBOOK-B leg 4).

bootstrap-env (SECRET-BEARING)

The --bootstrap-env file becomes the in-ISO /etc/felhom/bootstrap.env:

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 shreds 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)

Use the committed profiles/n100.profile template (cheap-board, FELHOM_LOADER="mkimage"). A real single-disk mini-PC uses 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. In the profile, set 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. Boot loader + Secure Boot (F1): cheap AMI (AN3PLUS-class) firmware can't boot the stock GRUB from USB, so profiles/n100.profile uses --loader mkimage. That loader is unsigned → set Secure Boot to OFF/Disabled in the BIOS before booting the stick (with SB enforcing the board refuses it: Access Denied, no boot). Firmware that boots the stock ISO fine needs no mkimage and keeps Secure Boot working (default shim, spike S2b). Optional pre-flight (operator, no risk): a n100-safety ISO — --loader mkimage + a match-nothing filter — boots the board, loads GRUB, and the installer aborts touching nothing (S5c). It confirms the loader + SB-off setting before the real install; its sha is recorded at build. Never boot the real install-armed ISO except in the supervised rehearsal.
  5. BIOS "State After G3" → "Power On" (or "Last State") before final sign-off — so the mini-PC auto-powers-on after a mains loss (a managed appliance must come back without a physical press). Set it in the BIOS power/ACPI menu; it is board-specific and cannot be scripted (operator step).

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).