iso v1.26.0: the PUBLIC release image — no answer file, interactive install, day-0 by .deb

Design inputs: SPIKE-universal-iso-{1,2,3,4}-2026-07-31.md. Every choice below is a measurement.

NEW: scripts/iso/pkg/ — the felhom-bootstrap .deb, built from committed source.
  Two files only (script + unit), NOT three: felhom-bootstrap.sh:91 reads /etc/felhom/bootstrap.env
  only 'if [[ -r ]]', and its defaults at :95-96 are EXACTLY what the pairing env set
  (build-felhom-iso.sh:257-258) — so shipping it would add a 0600 file to a public package to express
  values the script already defaults to. NO dependencies: the binaries it calls run at FIRST BOOT,
  not at postinst time, so SPIKE 4's open 'dpkg --configure -a' ordering question does not arise.
  The postinst is structurally incapable of failing (no 'set -e', every statement guarded, ends
  'exit 0'); build-deb.sh self-asserts G8/G9 and REFUSES to emit a package that violates them.

iso-repack.sh — two changes, both narrowing rather than deleting:
  - R-155 guard: now applies to FELHOM_MENU=single ONLY. It protected the single-entry mode's promise
    (one button labelled 'install' must not drop into a disk-picker); a release image carries no
    auto-installer-mode.toml BY DESIGN (gate G1), so refusing it would be the guard firing on the
    shape it describes rather than the one it prevents.
  - the menu collapse now has a release mode: two INTERACTIVE entries, Graphical default, timeout 15.
    Entry-count and banned-token gates are per-mode; the six-token list is UNCHANGED for single mode.
  - .deb injection into /proxmox/packages/, with a skip-list collision check (a colliding name would
    be dropped silently — the inert-payload class) and a post-remaster assertion that it landed in
    final.iso, not merely in the extract tree.

build-felhom-iso.sh — --release: no profile, no root hash, no answer.toml, no prepare-iso at all.
  Skipping prepare-iso is what removes the Automated entry by construction, since the stock grub.cfg
  emits it only inside 'if [ -f auto-installer-mode.toml ]'.

R-128 RULING — FIXED, by correcting the claim rather than inventing an assertion for it. The comment
  said ISO_VERSION 'aligns with SCRIPT_VERSION'; nothing evaluated it and the two had drifted. The
  coupling does not exist: the ISO is frozen, felhom-host-install.sh is fetched at run time from main
  (R-94/R-110), so an assertion would invent a constraint. Comment corrected, ISO_VERSION -> 1.26.0.

Release gate G6 AMENDED before the build, with its reasoning recorded in the runbook: the six-token
  ban existed to keep users away from the manual installer, which the ruling makes the product.
  'proxtui' (the TUI installer we ship) and 'nomodeset' (its graphics fallback) are dropped for
  release images; proxdebug/Rescue Boot/memtest/fwsetup stay banned in both modes.
This commit is contained in:
2026-07-31 16:39:47 +02:00
parent e787391c0a
commit 01a8155c5a
7 changed files with 439 additions and 38 deletions
+23 -4
View File
@@ -100,13 +100,32 @@ install paths on unfamiliar hardware needs to read them first. *Why the spelling
writes `timeout-style`, which GRUB does not recognise as a variable name at all (Spike 2 §5.2) —
Felhom's underscore form is the one that works.
**Also assert the safety property still holds** — no live route to a manual disk-picker via a debug or
rescue entry:
**Also assert the safety property still holds:**
```bash
grep -v '^[[:space:]]*#' /tmp/g.cfg | grep -cE 'proxtui|proxdebug|nomodeset|Rescue Boot|memtest|fwsetup'
grep -v '^[[:space:]]*#' /tmp/g.cfg | grep -cE 'proxdebug|Rescue Boot|memtest|fwsetup'
```
**PASS = `0`.** This is the gate `iso-repack.sh:160-164` already enforces and it is kept unchanged.
**PASS = `0`.**
> **Amendment, 2026-07-31, before the first build — recorded rather than made quietly.**
> This criterion was first written as the six-token list `iso-repack.sh:160-164` enforces
> (`proxtui|proxdebug|nomodeset|Rescue Boot|memtest|fwsetup`), on the stated rationale *"no live route
> to a manual disk-picker"*. **That rationale is obsolete for a public image and was already obsolete
> when it was written.** The operator's ruling makes the manual installer *the product*: the person
> installing chooses their own disk in the stock installer, which shows the target and a `Bootdisk(s)`
> summary before erasing. A token list whose purpose is to keep users away from the manual installer
> cannot be applied to an image whose purpose is to offer it.
>
> Two tokens are therefore dropped **for the release image only**:
> - **`proxtui`** — it is the Terminal UI *installer*, one of the two entries the image deliberately
> ships; and
> - **`nomodeset`** — a graphics fallback for the same installer, needed on hardware whose GPU the
> default mode cannot drive.
>
> **Four are kept, and they are the ones that were ever the real hazard:** `proxdebug` (drops to a
> shell), `Rescue Boot` (boots an existing system rather than installing), `memtest` and `fwsetup`
> (not installers at all). The original six-token gate remains **unchanged** for the single-entry
> appliance mode, where its original rationale still holds exactly.
### G7 — The Felhom package is present, at a recorded version
+84 -12
View File
@@ -41,7 +41,15 @@
#===============================================================================
set -euo pipefail
ISO_VERSION="1.25.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION).
# R-128 RULING (2026-07-31): the previous comment here claimed this "aligns with felhom-host-install
# SCRIPT_VERSION". That was a COMMENT, not an assertion — `SCRIPT_VERSION` appeared nowhere in
# scripts/iso/ except inside that sentence, so nothing ever evaluated it, and the two had drifted
# (1.25.0 vs 1.22.0). The fix is NOT to turn it into a real assertion, because the coupling it claimed
# does not exist: the ISO is a frozen artifact, while felhom-host-install.sh is fetched at RUN TIME
# from the website's git-sync of `main` (R-94/R-110), so whatever version an ISO carries, the script a
# box runs is always current. Coupling them would invent a constraint. The claim is corrected instead.
ISO_VERSION="1.26.0" # the ISO's own version. INDEPENDENT of felhom-host-install.sh's SCRIPT_VERSION,
# which is fetched at run time from main and is not frozen into the image.
IMAGE="${FELHOM_ISO_ASSISTANT_IMAGE:-felhom-iso-assistant:trixie}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -82,12 +90,15 @@ assert_golden_ge_floor() {
PVE_ISO=""; ISO_SHA256=""; PROFILE=""; BOOTSTRAP_ENV=""; OUT_DIR="${FELHOM_ISO_OUT:-/mnt/5_hdd/felhom.eu/felhom-iso/out}"; PVE_VERSION=""; DRY_RUN=false
LOADER_CLI="" # --loader override; empty = fall back to the profile, then the shim default.
PAIRING=false # --pairing: build the GENERIC secret-free ISO (slice C); no --bootstrap-env.
RELEASE=false # --release: the PUBLIC image. No answer.toml at all, an INTERACTIVE two-entry menu,
# and day-0 delivered by a .deb rather than the answer file's [first-boot] hook.
BRAND=true # R-38: Felhom boot screen + single-entry menu. --no-brand is a debugging escape only.
BRAND_IMAGE="" # --brand-image override; empty = the website's og-image_2.png (the single source).
usage() {
cat <<EOF
Usage (direct): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --bootstrap-env FILE [options]
Usage (generic): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --pairing [options]
Usage (release): build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --release [options]
Required:
--pve-iso PATH pre-downloaded official PVE ISO (not fetched here)
@@ -123,6 +134,7 @@ while [[ $# -gt 0 ]]; do
--profile) PROFILE="$2"; shift 2 ;;
--bootstrap-env) BOOTSTRAP_ENV="$2"; shift 2 ;;
--pairing) PAIRING=true; shift ;;
--release) RELEASE=true; shift ;;
--loader) LOADER_CLI="$2"; shift 2 ;;
--no-brand) BRAND=false; shift ;;
--brand-image) BRAND_IMAGE="$2"; shift 2 ;;
@@ -138,9 +150,18 @@ done
[[ -n "$PVE_ISO" ]] || die "--pve-iso is required"
[[ -n "$ISO_SHA256" ]] || die "--iso-sha256 is required"
[[ -n "$PROFILE" ]] || die "--profile is required"
[[ -f "$PVE_ISO" ]] || die "--pve-iso not found: $PVE_ISO"
[[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE"
if $RELEASE; then
# A release image has no answer file, so it has no fqdn and no [disk-setup] to take from a profile.
# Accepting one would imply a disk selection the image deliberately does not make.
[[ -z "$PROFILE" ]] || die "--release takes no --profile (a release image bakes no disk selection)"
[[ -z "$BOOTSTRAP_ENV" ]] || die "--release and --bootstrap-env are mutually exclusive"
$PAIRING && die "--release and --pairing are mutually exclusive"
$BRAND || die "--release requires branding (the two-entry menu IS the release menu)"
else
[[ -n "$PROFILE" ]] || die "--profile is required"
[[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE"
fi
# Mode: exactly one of --bootstrap-env (direct, secret-bearing) or --pairing (generic, secret-free).
if $PAIRING; then
[[ -z "$BOOTSTRAP_ENV" ]] || die "--pairing and --bootstrap-env are mutually exclusive"
@@ -165,6 +186,7 @@ fi
PROFILE_NAME="$(basename "$PROFILE")"; PROFILE_NAME="${PROFILE_NAME%.profile}"
# --- load + validate profile ----------------------------------------------------------------------
if ! $RELEASE; then
log_step "loading profile: $PROFILE"
FELHOM_FQDN=""; FELHOM_DISK_SETUP=""; FELHOM_ROOT_SSH_KEY=""; FELHOM_LOADER=""; FELHOM_HUB_URL=""; FELHOM_INSTALL_URL=""
# shellcheck disable=SC1090
@@ -174,6 +196,10 @@ source "$PROFILE"
# Optional emergency/validation SSH key baked into the installed root account.
ROOT_SSH_LINE=""
[[ -n "$FELHOM_ROOT_SSH_KEY" ]] && ROOT_SSH_LINE="root-ssh-keys = [\"${FELHOM_ROOT_SSH_KEY}\"]"
else
log_info "release mode — no profile, no fqdn, no [disk-setup], no root key"
FELHOM_FQDN="(none — interactive install)"
fi
# --- resolve the loader mode: --loader wins over the profile's FELHOM_LOADER over the shim default ---
LOADER="${LOADER_CLI:-${FELHOM_LOADER:-shim}}"
@@ -205,7 +231,14 @@ else
fi
# --- mode: DIRECT validates the secret-bearing env; PAIRING is secret-free (env generated below) -----
if $PAIRING; then
if $RELEASE; then
SECRET_BEARING="no"
echo -e "${YELLOW}==================================================================================${NC}"
log_info "RELEASE MODE — the PUBLIC image. No answer.toml, no auto-installer-mode.toml, no root"
log_info "password, no SSH key, no disk profile. Two INTERACTIVE menu entries; the person"
log_info "installing chooses the disk. Day-0 rides felhom-bootstrap.deb in /proxmox/packages/."
echo -e "${YELLOW}==================================================================================${NC}"
elif $PAIRING; then
SECRET_BEARING="no"
PAIR_HUB_URL="${FELHOM_HUB_URL:-https://hub.felhom.eu}"
PAIR_INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}"
@@ -265,9 +298,20 @@ fi
# Secure-Boot-off prep. -----------------------------------------------------------------------
LOADER_SUFFIX=""; [[ "$LOADER" != "shim" ]] && LOADER_SUFFIX="-${LOADER}"
MODE_SUFFIX=""; $PAIRING && MODE_SUFFIX="-generic" # the secret-free universal ISO is unmistakable
OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}${MODE_SUFFIX}${LOADER_SUFFIX}.iso"
if $RELEASE; then
# Named so it cannot be mistaken for one of the lab artifacts in out/ (canary, probe, nested-vm,
# demo, hp, n100). "felhom-installer" + the version is what a tester quotes in a bug report.
OUT_ISO="$OUT_DIR/felhom-installer-${ISO_VERSION}-pve${PVE_VERSION}${LOADER_SUFFIX}.iso"
else
OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}${MODE_SUFFIX}${LOADER_SUFFIX}.iso"
fi
# --- mint fresh THROWAWAY root hash ---------------------------------------------------------------
if $RELEASE; then
log_info "release mode — NOT minting a root password (release gate G2: none is baked, and no"
log_info " .rootpw.txt is emitted). The installer prompts the person doing the install."
ROOT_HASH=""
else
log_step "minting fresh throwaway root password hash"
ROOT_PLAIN="felhom-throwaway-$(head -c12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9')"
ROOT_HASH="$(openssl passwd -6 "$ROOT_PLAIN")"
@@ -281,8 +325,10 @@ ROOT_HASH="$(openssl passwd -6 "$ROOT_PLAIN")"
unset ROOT_PLAIN
[[ -n "$ROOT_HASH" ]] || die "failed to mint root hash"
log_info "throwaway root hash written to the answer file; plaintext in $(basename "$OUT_ISO").rootpw.txt (0600, operator-only — value not logged)"
fi
# --- render answer.toml (pure bash param-expansion; no metachar hazards) ---------------------------
if ! $RELEASE; then
log_step "rendering answer.toml"
ANSWER="$WORK/answer.toml"
: > "$ANSWER"
@@ -329,6 +375,17 @@ awk -v sh="$sh_b64" -v unit="$unit_b64" -v env="$env_b64" '
' "$HERE/stub-first-boot.sh" > "$STUB"
chmod 0755 "$STUB"
grep -q '@@BOOTSTRAP_.*_B64@@' "$STUB" && die "stub still has unfilled markers — injection failed"
fi # end: non-release answer/stub rendering
# --- RELEASE: build the felhom-bootstrap .deb from COMMITTED source (release gate G7/G9/G10) --------
RELEASE_DEB=""
if $RELEASE; then
log_step "building the felhom-bootstrap package"
[[ -x "$HERE/pkg/build-deb.sh" ]] || die "pkg/build-deb.sh not found next to build-felhom-iso.sh"
RELEASE_DEB="$("$HERE/pkg/build-deb.sh" "$ISO_VERSION" "$WORK/pkg")" \
|| die "package build FAILED (its own G8/G9 self-assertions gate it)"
log_success "package: $(basename "$RELEASE_DEB") ($(stat -c%s "$RELEASE_DEB") bytes)"
fi
# --- prepare-iso ----------------------------------------------------------------------------------
# (OUT_ISO is resolved above the mint — the rootpw sibling file is named after it.)
@@ -342,11 +399,19 @@ if $DRY_RUN; then
log_info "DRY-RUN: no ISO produced"
exit 0
fi
docker run --rm -v "$ISO_DIR":/iso:ro -v "$WORK":/work "$IMAGE" \
proxmox-auto-install-assistant prepare-iso "/iso/$ISO_BASE" \
--fetch-from iso --answer-file /work/answer.toml \
--on-first-boot /work/stub-first-boot.sh \
--tmp /work/tmp --output /work/out.iso
if $RELEASE; then
# NO prepare-iso. That step is what creates /answer.toml and /auto-installer-mode.toml, and their
# absence is release-gate G1 — it also removes the Automated menu entry by construction, since the
# stock grub.cfg emits it only inside `if [ -f auto-installer-mode.toml ]`.
log_info "release mode — skipping prepare-iso entirely (no answer file is created)"
cp "$PVE_ISO" "$WORK/out.iso"
else
docker run --rm -v "$ISO_DIR":/iso:ro -v "$WORK":/work "$IMAGE" \
proxmox-auto-install-assistant prepare-iso "/iso/$ISO_BASE" \
--fetch-from iso --answer-file /work/answer.toml \
--on-first-boot /work/stub-first-boot.sh \
--tmp /work/tmp --output /work/out.iso
fi
[[ -f "$WORK/out.iso" ]] || die "prepare-iso produced no output"
# --- REPACK: R-38 GRUB branding + single-entry menu, and (slice B) the mkimage loader surgery. Both
@@ -389,6 +454,7 @@ BRAND_NOTE="FELHOM (single entry 'Felhom telepítés', default, 5s; stock instal
$BRAND || BRAND_NOTE="STOCK PVE menu (--no-brand; DEBUG BUILD — manual installer reachable, do not ship)"
MODE_NOTE="direct (env-baked customer-id + retrieval passphrase; secret-bearing)"
$PAIRING && MODE_NOTE="pairing (GENERIC secret-free universal ISO — box self-registers, operator binds, hub delivers once)"
$RELEASE && MODE_NOTE="release (PUBLIC image — NO answer.toml, NO baked credential, interactive disk selection)"
cat > "$OUT_ISO.manifest.txt" <<EOF
Felhom bare-metal ISO build manifest (R-21 slice A+B+C)
built : $(date -Is)
@@ -406,7 +472,10 @@ boot-menu : ${BRAND_NOTE}
$( $BRAND && cat "$WORK/brand-report.txt" 2>/dev/null || true )
host-install-url : $(grep -oE 'FELHOM_INSTALL_URL=[^ ]*' "$BOOTSTRAP_ENV" 2>/dev/null || echo 'https://felhom.eu/scripts/felhom-host-install.sh (default)')
secret-bearing : ${SECRET_BEARING}$( $PAIRING && echo ' (GENERIC ISO — carries NO customer secret)' || echo ' (embeds the customer retrieval passphrase — supervised/single-use, delete after the run)')
root-password : see $(basename "$OUT_ISO").rootpw.txt (operator-only; NEVER commit or paste into REPORTs)
root-password : $( $RELEASE && echo 'NONE — not baked. The installer prompts the person installing (release gate G2).' || echo "see $(basename "$OUT_ISO").rootpw.txt (operator-only; NEVER commit or paste into REPORTs)" )
answer-file : $( $RELEASE && echo 'NONE — no answer.toml, no auto-installer-mode.toml (release gate G1)' || echo 'baked (/answer.toml)' )
felhom-package : $( [[ -n "$RELEASE_DEB" ]] && echo "$(basename "$RELEASE_DEB") sha256=$(sha256sum "$RELEASE_DEB" | cut -d\ -f1)" || echo 'n/a (day-0 rides the answer file first-boot hook)' )
repo-commit : $(cd "$HERE" && git rev-parse HEAD 2>/dev/null || echo unknown)
output : $(basename "$OUT_ISO")
output-sha256 : ${OUT_SHA}
output-size-bytes : ${OUT_SIZE}
@@ -417,7 +486,10 @@ log_info "sha256 : $OUT_SHA"
log_info "size : $OUT_SIZE bytes"
log_info "manifest : $OUT_ISO.manifest.txt"
log_info "root-pw : $OUT_ISO.rootpw.txt (0600, operator-only — the console credential for this build; never commit/paste)"
if $PAIRING; then
if $RELEASE; then
log_success "PUBLIC RELEASE image — no answer file, no baked credential, interactive disk selection."
log_info "Run the release gate before publishing: documentation/runbooks/iso-release-gate.md"
elif $PAIRING; then
log_success "GENERIC secret-free ISO — carries NO customer secret. Bind the box on the hub after it registers."
else
log_warn "SECRET-BEARING ISO (embeds the retrieval passphrase). Supervised/single-use; never distribute; delete after the run."
+80
View File
@@ -0,0 +1,80 @@
# Felhom PUBLIC RELEASE ISO — GRUB menu. GENERATED by iso-repack.sh (FELHOM_MENU=release).
#
# TWO entries, both INTERACTIVE, and that is the whole design.
#
# WHY NOT THE AUTOMATED ENTRY: SPIKE-universal-iso-1 measured that no udev property distinguishes an
# internal system disk from a customer's external backup drive (ID_BUS is `ata` for BOTH on a SATA
# drive in a USB caddy; no removability property is exposed at all; NVMe carries a different property
# set), and that a filter matching two disks does NOT fail safe — the installer silently picks one and
# wipes it, while validate-answer accepts the answer. There is therefore no safe automated disk
# selection for hardware nobody has seen. The stock installer already shows the target disk and a
# `Bootdisk(s)` summary before erasing, so the human doing the choosing is the safety mechanism.
#
# WHY THE GRAPHICAL ENTRY IS DEFAULT: a tester who boots the stick and walks away must not land on a
# destructive unattended path. The default is the one that waits for a person.
#
# WHY timeout=15: SPIKE-universal-iso-2 lost an entire probe to the installed system's 1-SECOND menu.
# A human reading two options on unfamiliar hardware needs longer than a machine does. Note
# `timeout_style` (underscore) — the stock PVE config writes `timeout-style`, which GRUB does not
# recognise as a variable name at all, so the stock menu's own timeout directive is inert.
#
# The `linux` / `initrd` lines are lifted VERBATIM by iso-repack.sh from the stock ISO's own Graphical
# and Terminal-UI entries, so a PVE version bump that changes the kernel path tracks automatically
# instead of silently diverging from a copy frozen here.
insmod gzio
insmod iso9660
insmod png
insmod gfxmenu
if [ x$feature_default_font_path = xy ] ; then
font=unicode
else
font=$prefix/unicode.pf2
fi
set gfxmode=1024x768,800x600,640x480
set gfxpayload=1024x768
if loadfont $font; then
if test "${grub_platform}" = "efi"; then
insmod efi_gop
insmod efi_uga
fi
insmod video_bochs
insmod video_cirrus
insmod all_video
insmod gfxterm
set theme=/boot/grub/felhomtheme/theme.txt
export theme
terminal_input console
terminal_output gfxterm
fi
insmod serial
insmod usbserial_common
insmod usbserial_ftdi
insmod usbserial_pl2303
insmod usbserial_usbdebug
if serial --unit=0 --speed=115200; then
terminal_input --append serial
terminal_output --append serial
fi
set timeout_style=menu
set timeout=15
set default=0
menuentry 'Felhom telepítés' --class felhom --class os {
echo 'A Felhom telepítése indul — válassza ki a lemezt a telepítőben...'
@@LINUX_GFX@@
echo 'Rendszerbetöltő betöltése...'
@@INITRD@@
}
menuentry 'Felhom telepítés (szöveges mód)' --class felhom --class os {
echo 'A Felhom telepítése indul szöveges módban...'
@@LINUX_TUI@@
echo 'Rendszerbetöltő betöltése...'
@@INITRD@@
}
+116 -22
View File
@@ -15,6 +15,12 @@
# Env:
# FELHOM_LOADER = shim | mkimage (default shim) — mkimage swaps BOOTX64.EFI (F1 firmware fix)
# FELHOM_BRAND = 1 | 0 (default 1) — 0 leaves the stock PVE menu completely alone
# FELHOM_MENU = single | release (default single) — single: ONE automated entry (appliance ISO,
# requires a prepared auto-install ISO). release: TWO INTERACTIVE
# entries for the PUBLIC image, which carries no answer.toml.
# FELHOM_DEB = <path> (optional) — a .deb to inject into /proxmox/packages/, which
# the PVE installer unpacks into the target on EVERY install path
# including the interactive one (Install.pm:1343-1372, :1378).
#
# Expects in /work (placed by build-felhom-iso.sh) when FELHOM_BRAND=1:
# brand/grub.cfg.tmpl, brand/felhom-theme.txt, brand/generate-grub-background.sh, brand/card.png
@@ -39,6 +45,10 @@ ORIG_CFG=/work/grub.cfg.orig
LOADER="${FELHOM_LOADER:-shim}"
BRAND="${FELHOM_BRAND:-1}"
MENU="${FELHOM_MENU:-single}"
DEB="${FELHOM_DEB:-}"
[[ "$MENU" == "single" || "$MENU" == "release" ]] \
|| { echo "iso-repack: FELHOM_MENU must be single|release (got '$MENU')" >&2; exit 2; }
say() { echo "iso-repack: $*"; }
@@ -95,15 +105,27 @@ cp "$GCFG" "$ORIG_CFG"
if [[ "$BRAND" == "1" ]]; then
say "branding GRUB (background + single-entry menu)"
# 2a. The prepared ISO MUST be an auto-install ISO. Our one menu entry boots the automated
# installer, which reads this file; without it the entry would drop into the manual installer
# — the exact outcome the single-entry menu exists to prevent. Fail loudly, never silently.
AIM="$(find "$EX" -maxdepth 2 -iname 'auto-installer-mode.toml' | head -1)"
[[ -n "$AIM" ]] || {
echo "iso-repack: auto-installer-mode.toml not found in the ISO — this is not a prepared" >&2
echo " auto-install ISO, so the single Felhom entry would boot the MANUAL installer. Refusing." >&2
exit 10
}
# 2a. R-155 — the guard, NARROWED rather than removed.
#
# What it protected: in SINGLE-entry mode the menu shows exactly one item, "Felhom telepítés",
# and that item boots the AUTOMATED installer, which reads auto-installer-mode.toml. Without
# that file the very same label would drop the user into the manual disk-picker — a button
# promising an unattended install that silently does the opposite. That promise is real and
# the guard still enforces it, unchanged, for FELHOM_MENU=single.
#
# Why it must not apply to FELHOM_MENU=release: the public image deliberately offers the
# interactive installer (see grub-release.cfg.tmpl for the measurements behind that ruling),
# so it carries NO answer.toml and NO auto-installer-mode.toml by design — that absence is a
# release-gate criterion (G1), not a defect. Refusing it would be the guard firing on the
# shape it was written to describe rather than the shape it was written to prevent.
if [[ "$MENU" == "single" ]]; then
AIM="$(find "$EX" -maxdepth 2 -iname 'auto-installer-mode.toml' | head -1)"
[[ -n "$AIM" ]] || {
echo "iso-repack: auto-installer-mode.toml not found in the ISO — this is not a prepared" >&2
echo " auto-install ISO, so the single Felhom entry would boot the MANUAL installer. Refusing." >&2
exit 10
}
fi
# 2b. Lift the kernel + initrd lines VERBATIM from the stock 'Install Proxmox VE (Automated)'
# entry, so a PVE bump that changes the kernel path or append line tracks automatically.
@@ -118,14 +140,36 @@ if [[ "$BRAND" == "1" ]]; then
inblk && /^[[:space:]]*}/ { inblk=0 }
' "$ORIG_CFG")"
[[ -n "$LINUX_LINE" ]] || { echo "iso-repack: could not lift the 'linux' line from the stock automated entry" >&2; exit 11; }
# release mode lifts the GRAPHICAL and TERMINAL-UI kernel lines instead of the automated one.
LINUX_GFX="$(awk '
/menuentry .Install Proxmox VE \(Graphical\)./ { inblk=1; next }
inblk && /^[[:space:]]*linux[[:space:]]/ { print; exit }
inblk && /^[[:space:]]*}/ { inblk=0 }
' "$ORIG_CFG")"
LINUX_TUI="$(awk '
/menuentry .Install Proxmox VE \(Terminal UI\)./ { inblk=1; next }
inblk && /^[[:space:]]*linux[[:space:]]/ { print; exit }
inblk && /^[[:space:]]*}/ { inblk=0 }
' "$ORIG_CFG")"
if [[ "$MENU" == "release" ]]; then
[[ -n "$LINUX_GFX" ]] || { echo "iso-repack: could not lift the Graphical 'linux' line" >&2; exit 11; }
[[ -n "$LINUX_TUI" ]] || { echo "iso-repack: could not lift the Terminal-UI 'linux' line" >&2; exit 11; }
# The release menu must NOT carry the unattended flag — that is the whole point of the ruling.
grep -q 'proxmox-start-auto-installer' <<<"$LINUX_GFX$LINUX_TUI" && {
echo "iso-repack: a release menu kernel line carries proxmox-start-auto-installer — refusing" >&2
exit 12
}
fi
[[ -n "$LINUX_LINE" || "$MENU" == "release" ]] || { echo "iso-repack: could not lift the 'linux' line from the stock automated entry" >&2; exit 11; }
[[ -n "$INITRD_LINE" ]] || { echo "iso-repack: could not lift the 'initrd' line from the stock automated entry" >&2; exit 11; }
# The append flag that MAKES it unattended. If PVE ever renames it, we must not ship an ISO that
# boots a manual installer behind a button labelled "Felhom telepítés".
grep -q 'proxmox-start-auto-installer' <<<"$LINUX_LINE" || {
echo "iso-repack: the lifted kernel line has no 'proxmox-start-auto-installer' flag:" >&2
echo " $LINUX_LINE" >&2; exit 12
}
if [[ "$MENU" == "single" ]]; then
grep -q 'proxmox-start-auto-installer' <<<"$LINUX_LINE" || {
echo "iso-repack: the lifted kernel line has no 'proxmox-start-auto-installer' flag:" >&2
echo " $LINUX_LINE" >&2; exit 12
}
fi
grep -q '/boot/initrd.img' <<<"$INITRD_LINE" || {
echo "iso-repack: the lifted initrd line looks wrong: $INITRD_LINE" >&2; exit 12
}
@@ -143,21 +187,39 @@ if [[ "$BRAND" == "1" ]]; then
# 2d. Render the new grub.cfg. Use awk (not sed) so the lifted lines are inserted literally —
# the append line is full of `/` and `=` that sed would need escaped.
awk -v lx="$LINUX_NORM" -v ird="$INITRD_NORM" '
{ gsub(/@@LINUX@@/, lx); gsub(/@@INITRD@@/, ird); print }
' "$BRANDDIR/grub.cfg.tmpl" > "$GCFG"
grep -q '@@LINUX@@\|@@INITRD@@' "$GCFG" && { echo "iso-repack: grub.cfg still has unfilled markers" >&2; exit 13; }
if [[ "$MENU" == "release" ]]; then
GFX_NORM=" $(sed -E 's/^[[:space:]]+//' <<<"$LINUX_GFX")"
TUI_NORM=" $(sed -E 's/^[[:space:]]+//' <<<"$LINUX_TUI")"
awk -v gfx="$GFX_NORM" -v tui="$TUI_NORM" -v ird="$INITRD_NORM" '
{ gsub(/@@LINUX_GFX@@/, gfx); gsub(/@@LINUX_TUI@@/, tui); gsub(/@@INITRD@@/, ird); print }
' "$BRANDDIR/grub-release.cfg.tmpl" > "$GCFG"
else
awk -v lx="$LINUX_NORM" -v ird="$INITRD_NORM" '
{ gsub(/@@LINUX@@/, lx); gsub(/@@INITRD@@/, ird); print }
' "$BRANDDIR/grub.cfg.tmpl" > "$GCFG"
fi
grep -q '@@LINUX@@\|@@LINUX_GFX@@\|@@LINUX_TUI@@\|@@INITRD@@' "$GCFG" && { echo "iso-repack: grub.cfg still has unfilled markers" >&2; exit 13; }
# 2e. GATES — the safety half is the whole point, so assert it on the rendered file rather than
# trusting the template. Exactly one entry, zero submenus, no path back to a manual installer.
N_ENTRY="$(grep -c '^[[:space:]]*menuentry ' "$GCFG" || true)"
N_SUB="$(grep -c '^[[:space:]]*submenu ' "$GCFG" || true)"
[[ "$N_ENTRY" == "1" ]] || { echo "iso-repack: rendered grub.cfg has $N_ENTRY menuentries, want exactly 1" >&2; exit 14; }
WANT_ENTRIES=1; [[ "$MENU" == "release" ]] && WANT_ENTRIES=2
[[ "$N_ENTRY" == "$WANT_ENTRIES" ]] || { echo "iso-repack: rendered grub.cfg has $N_ENTRY menuentries, want exactly $WANT_ENTRIES ($MENU mode)" >&2; exit 14; }
[[ "$N_SUB" == "0" ]] || { echo "iso-repack: rendered grub.cfg has $N_SUB submenus, want 0" >&2; exit 14; }
# Strip comments first: the template's header EXPLAINS which stock entries were dropped, and
# naming them there must not trip the gate. What matters is that no live directive uses them.
LIVE="$(grep -v '^[[:space:]]*#' "$GCFG")"
for banned in proxtui proxdebug nomodeset 'Rescue Boot' memtest fwsetup; do
# The banned set differs by mode, and the difference is the whole ruling (release-gate G6 amendment):
# single — six tokens. The one entry promises an unattended install, so ANY route to the manual
# installer breaks that promise. Unchanged.
# release — four tokens. The manual installer IS the product here, so `proxtui` (the Terminal-UI
# installer, one of the two entries we ship) and `nomodeset` (a graphics fallback for
# the same installer) are legitimate. What stays banned is what never installs anything:
# a debug shell, a rescue boot of an existing system, memtest and firmware settings.
BANNED=(proxtui proxdebug nomodeset 'Rescue Boot' memtest fwsetup)
[[ "$MENU" == "release" ]] && BANNED=(proxdebug 'Rescue Boot' memtest fwsetup)
for banned in "${BANNED[@]}"; do
if grep -q "$banned" <<<"$LIVE"; then
echo "iso-repack: rendered grub.cfg still has a live reference to '$banned'" >&2; exit 14
fi
@@ -267,6 +329,30 @@ CFG
say "swapped bootx64.efi in the ISO tree ($TREE_HITS) and inside $(basename "$EFIIMG")"
fi
# --- 5b. INJECT the Felhom package into /proxmox/packages/ (release images).
#
# Install.pm:1343-1372 unpacks EVERY .deb in that directory into the target, with a fixed
# skip-list of known package-name patterns, then :1378 runs `dpkg --configure -a` which executes
# the postinsts. This happens on EVERY install path — measured on the interactive one in
# SPIKE-universal-iso-4 (package installed, postinst run, unit enabled, unit fired at 7.98 s
# uptime, with proxmox-first-boot absent on the same machine). It is the ONLY delivery mechanism
# that survives an interactive install; the answer file's [first-boot] hook does not.
if [[ -n "$DEB" ]]; then
[[ -f "$DEB" ]] || { echo "iso-repack: FELHOM_DEB not found: $DEB" >&2; exit 16; }
PKGDIR="$EX/proxmox/packages"
[[ -d "$PKGDIR" ]] || { echo "iso-repack: $PKGDIR missing — not a PVE ISO?" >&2; exit 16; }
cp "$DEB" "$PKGDIR/$(basename "$DEB")"
say "injected $(basename "$DEB") into /proxmox/packages/ ($(ls "$PKGDIR"/*.deb | wc -l) debs total)"
# The skip-list at Install.pm:1352-1362 matches known package-name prefixes. A Felhom package must
# not collide with one, or it would be silently skipped on some hardware and the whole delivery
# would fail invisibly — exactly the inert-payload class this project keeps hitting.
case "$(basename "$DEB")" in
grub-pc_*|grub-efi-*|proxmox-grub*|proxmox-secure-boot-support_*|proxmox-first-boot*|amd64-microcode_*|intel-microcode_*)
echo "iso-repack: package name collides with the installer's skip-list — it would be dropped" >&2
exit 16 ;;
esac
fi
# --- 6. re-master from the (modified) tree, reproducing the source ISO's boot geometry from its OWN
# as_mkisofs report so we track PVE minor versions. We drop ONLY the Apple APM/HFS+ boot map
# (-hfsplus / -apm-block-size): re-emitting it trips xorriso 1.5.6's "Overlapping MBR partition
@@ -314,10 +400,18 @@ if [[ "$BRAND" == "1" ]]; then
xorriso -osirrox on -indev "$FINAL" -extract /boot/grub/grub.cfg "$VER/grub.cfg" >/dev/null 2>&1
xorriso -osirrox on -indev "$FINAL" -extract /boot/grub/felhomtheme "$VER/felhomtheme" >/dev/null 2>&1
N="$(grep -c '^[[:space:]]*menuentry ' "$VER/grub.cfg" 2>/dev/null || echo 0)"
[[ "$N" == "1" ]] || { echo "iso-repack: final.iso menu has $N entries, want 1" >&2; exit 15; }
[[ "$N" == "$WANT_ENTRIES" ]] || { echo "iso-repack: final.iso menu has $N entries, want $WANT_ENTRIES" >&2; exit 15; }
[[ -s "$VER/felhomtheme/background.png" ]] || { echo "iso-repack: final.iso carries no theme background" >&2; exit 15; }
grep -q "Felhom telepítés" "$VER/grub.cfg" || { echo "iso-repack: final.iso menu entry is not the Felhom one" >&2; exit 15; }
rm -rf "$VER"
say "verified in final.iso: 1 entry ('Felhom telepítés') + theme background present"
say "verified in final.iso: $N entr(y/ies) + theme background present"
fi
# --- 8b. and prove the PACKAGE landed in final.iso too (seam-wiring: assert on what ships, not on the
# tree we built it from). --------------------------------------------------------------------
if [[ -n "$DEB" ]]; then
B="$(basename "$DEB")"
xorriso -indev "$FINAL" -find /proxmox/packages -maxdepth 1 2>/dev/null | grep -q "$B" \
|| { echo "iso-repack: $B is NOT in final.iso/proxmox/packages — delivery would be inert" >&2; exit 17; }
say "verified in final.iso: $B present in /proxmox/packages/"
fi
say "done"
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
#===============================================================================
# build-deb.sh — build the felhom-bootstrap .deb that the PUBLIC ISO carries.
#
# WHY A PACKAGE AND NOT THE ANSWER FILE'S [first-boot] HOOK:
# SPIKE-universal-iso-3 measured, with a same-image control, that an INTERACTIVE install never places
# the first-boot hook on the system at all — the proxmox-first-boot PACKAGE is not even installed
# (Config.pm:118 defaults first_boot.enabled=0, Install.pm:746 returns early, :1360 skips the package,
# and proxinstall contains zero occurrences of "first-boot"). SPIKE-universal-iso-4 then measured that
# a .deb in the ISO's /proxmox/packages/ IS delivered on that same interactive path — installed,
# postinst run, unit enabled, unit fired at 7.98 s uptime — because Install.pm:1343-1372 unpacks every
# .deb on the medium and :1378 configures them.
#
# CONTENTS — exactly two files, and deliberately not three:
# /usr/local/sbin/felhom-bootstrap.sh 0755 (byte-identical to scripts/iso/felhom-bootstrap.sh)
# /lib/systemd/system/felhom-bootstrap.service 0644
# The old stub also wrote /etc/felhom/bootstrap.env (0600). This package does NOT, because
# felhom-bootstrap.sh:91 reads it only `if [[ -r ... ]]` and its defaults at :95-96 are EXACTLY what
# the generic pairing env set (build-felhom-iso.sh:257-258). Shipping it would add a 0600 file to a
# public package to express values the script already defaults to.
#
# DEPENDENCIES: none, and that is a finding rather than an omission. The payload is a shell script and
# a unit file. The binaries the script calls (curl, ip, dhclient, python3, systemctl) run at FIRST
# BOOT, not at postinst time, and are all in a PVE base install — so there is nothing for
# `dpkg --configure -a` to order against, and SPIKE 4's open ordering question does not arise.
#===============================================================================
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ISO_DIR="$(cd "$HERE/.." && pwd)"
VERSION="${1:?usage: build-deb.sh <version> <outdir>}"
OUTDIR="${2:?usage: build-deb.sh <version> <outdir>}"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-deb.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
ROOT="$WORK/pkg"
install -d -m 0755 "$ROOT/DEBIAN" "$ROOT/usr/local/sbin" "$ROOT/lib/systemd/system"
sed "s/^Package: /Version: $VERSION\nPackage: /" /dev/null >/dev/null 2>&1 || true
{ head -1 "$HERE/debian/control"; echo "Version: $VERSION"; tail -n +2 "$HERE/debian/control"; } \
> "$ROOT/DEBIAN/control"
install -m 0755 "$HERE/debian/postinst" "$ROOT/DEBIAN/postinst"
# The two payload files, copied VERBATIM from the same sources the old stub embedded, so the ISO's
# frozen script is provably the repo's (release gate G9).
install -m 0755 "$ISO_DIR/felhom-bootstrap.sh" "$ROOT/usr/local/sbin/felhom-bootstrap.sh"
install -m 0644 "$ISO_DIR/felhom-bootstrap.service" "$ROOT/lib/systemd/system/felhom-bootstrap.service"
mkdir -p "$OUTDIR"
DEB="$OUTDIR/felhom-bootstrap_${VERSION}_all.deb"
dpkg-deb --build --root-owner-group "$ROOT" "$DEB" >/dev/null
# Self-assertions: the package must satisfy the release gate's G8/G9 before it ever reaches an ISO.
# Strip comments first — the postinst's header NAMES the forbidden verbs in order to explain why they
# are banned, and naming them there must not trip the gate. Same reasoning, and same fix, as
# iso-repack.sh:157-159 applies to the GRUB banned-token gate.
POST_RAW="$(dpkg-deb --ctrl-tarfile "$DEB" | tar -xO ./postinst)"
POST_LIVE="$(grep -v '^[[:space:]]*#' <<<"$POST_RAW")"
if grep -qE 'systemctl (start|daemon-reload|restart)' <<<"$POST_LIVE"; then
echo "build-deb: postinst has a LIVE forbidden systemctl verb (G8)" >&2; exit 3
fi
if grep -qE '^[[:space:]]*set -e' <<<"$POST_LIVE"; then
echo "build-deb: postinst uses 'set -e' (G8)" >&2; exit 3
fi
if grep -qE '\b(curl|wget|apt-get|nc|ping)\b' <<<"$POST_LIVE"; then
echo "build-deb: postinst uses the network (G8)" >&2; exit 3
fi
if [[ "$(tail -1 <<<"$POST_RAW")" != "exit 0" ]]; then
echo "build-deb: postinst does not end 'exit 0' (G8)" >&2; exit 3
fi
A="$(dpkg-deb --fsys-tarfile "$DEB" | tar -xO ./usr/local/sbin/felhom-bootstrap.sh | sha256sum | cut -d' ' -f1)"
B="$(sha256sum "$ISO_DIR/felhom-bootstrap.sh" | cut -d' ' -f1)"
if [[ "$A" != "$B" ]]; then
echo "build-deb: packaged felhom-bootstrap.sh != repo HEAD (G9)" >&2; exit 3
fi
echo "$DEB"
+15
View File
@@ -0,0 +1,15 @@
Package: felhom-bootstrap
Section: admin
Priority: optional
Architecture: all
Maintainer: Felhom <noreply@felhom.eu>
Description: Felhom host bootstrap — day-0 pairing on first boot
Installs the Felhom first-boot bootstrap and its systemd unit. On the first boot
of a freshly installed Proxmox VE host the unit registers the box at the Felhom
hub as an unclaimed appliance and waits to be bound; the operator binds it and
the hub delivers the credentials once.
.
This package carries NO customer identity and NO credential of any kind. It is
the delivery mechanism for a PUBLIC installation image: the PVE installer unpacks
every .deb on the medium into the target on every install path, including the
interactive one, which the [first-boot] answer-file hook does not reach.
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
# felhom-bootstrap postinst.
#
# THIS SCRIPT MUST NOT BE ABLE TO FAIL. It runs under `dpkg --configure -a` INSIDE THE PVE INSTALLER
# CHROOT, in the middle of a customer's installation. A non-zero exit surfaces to that customer as an
# install error — far worse than the bootstrap simply not running. The stub not running costs one
# documented command; a broken install costs the machine.
#
# Four constraints, every one MEASURED in SPIKE-universal-iso-4-2026-07-31.md §3:
# 1. NO `set -e` — it converts any unexpected non-zero into the failure above.
# 2. NO systemctl start / daemon-reload — pid1 in the chroot is `unconfigured.sh` and NO systemd is
# running; those verbs are meaningless there. `enable` is the only one
# that works, and it was measured to work (it wrote the symlink).
# 3. NO network use — the network was up in the probe ONLY because the installer's DHCP
# happened to hold. A box installed with the cable out has none.
# 4. Real work at first boot — the unit does it, where systemd, network and a booted kernel exist.
#
# Every statement below is therefore individually guarded and the script ends `exit 0` unconditionally.
UNIT=felhom-bootstrap.service
LOG=/var/log/felhom-bootstrap-postinst.log
# A positive observable that this ran. SPIKE 2 R-150: a hook that never ran is indistinguishable from
# one that succeeded if you only look for errors.
{
echo "felhom-bootstrap postinst: arg1=${1:-} date=$(date -u -Is 2>/dev/null || echo unknown)"
} >> "$LOG" 2>&1 || true
chmod 0644 "$LOG" 2>/dev/null || true
if [ "${1:-}" = "configure" ]; then
# `enable` only. Guarded, and its outcome recorded either way.
if systemctl enable "$UNIT" >> "$LOG" 2>&1; then
echo "felhom-bootstrap postinst: enabled $UNIT via systemctl" >> "$LOG" 2>&1 || true
else
# Fallback: write the wants-symlink by hand. Also guarded — if this fails too, the unit is
# simply not enabled and the operator runs the documented one-liner. The install still succeeds.
mkdir -p /etc/systemd/system/multi-user.target.wants 2>/dev/null || true
ln -sf "/lib/systemd/system/$UNIT" \
"/etc/systemd/system/multi-user.target.wants/$UNIT" 2>/dev/null || true
echo "felhom-bootstrap postinst: systemctl enable failed; wrote wants-symlink by hand" \
>> "$LOG" 2>&1 || true
fi
fi
exit 0