scripts v1.18.0 — R-21 slice B: firmware loader option --loader shim|mkimage (F1)

Closes N100 F1 (HIGH): cheap AMI (AN3PLUS 0.01-class) UEFI firmware can't
relocate the ISO's stock signed GRUB from USB (relocation 0x0). The run's live
grub-mkimage workaround is now a first-class pipeline mode.

- build-felhom-iso.sh: --loader shim|mkimage (default shim, byte-for-byte
  unchanged; profile-settable FELHOM_LOADER; --loader wins). Loud banner +
  manifest loader:/grub-mkimage: fields + -mkimage filename suffix.
- mkimage-surgery.sh (new): post-prepare-iso, in the assistant container. Builds
  a monolithic grub-mkimage loader from the ISO's own GRUB (module set from its
  grub.cfg; embedded search --fs-uuid -> configfile the real menu). Swaps it into
  the ISO9660 tree (real lowercase path) + the efi.img ESP; xorriso re-master
  preserves BIOS-hybrid + UEFI + GPT-ESP, drops only Apple HFS+/APM. Recipe from
  the N100 run evidence, not re-derived.
- Dockerfile.assistant: grub-common + grub-efi-amd64-bin + mtools + dosfstools.
  profiles/n100.profile (new, mkimage + SB-off note).
- Validated on nested VM 311 (RUNBOOK-B legs): leg1 shim boots+installs under
  OVMF SB-enforcing + SeaBIOS; leg2 mkimage boots+installs under SB-off; leg3
  (red-proof) mkimage under SB-enforcing FAILS Access Denied (unsigned -> SB must
  be OFF); leg4 surgery byte-identical payload. bash -n + shellcheck clean.
  Physical N100 closure folds into the rehearsal (n100-safety match-nothing ISO
  built + sha-recorded, unbooted). PXE stays a deferred R-21 note.
This commit is contained in:
2026-07-17 14:28:10 +02:00
parent d86d2699c3
commit 3172df1927
11 changed files with 439 additions and 58 deletions
+68 -8
View File
@@ -1,6 +1,6 @@
#!/bin/bash
#===============================================================================
# build-felhom-iso.sh — R-21 slice A: turn the official PVE ISO into a Felhom auto-install ISO.
# build-felhom-iso.sh — R-21 slice A+B: turn the official PVE ISO into a Felhom auto-install ISO.
#
# Renders answer.toml (from answer.toml.tmpl + a profile), mints a fresh THROWAWAY root hash,
# gates the answer through validate-answer by PARSING ITS OUTPUT (never $? — validate-answer returns
@@ -8,15 +8,26 @@
# script/unit/env), and runs prepare-iso --fetch-from iso --on-first-boot. Emits the ISO + sha256 +
# a build manifest.
#
# SLICE B — --loader shim|mkimage (default shim). shim = the stock output (MS-signed shim→GRUB chain,
# keeps Secure Boot working on compliant firmware, spike S2b). mkimage = replace the ISO's UEFI boot
# path with a monolithic grub-mkimage-built BOOTX64.EFI built from the ISO's OWN GRUB modules — the
# workaround PROVEN LIVE during the N100 run (VALIDATION-n100-baremetal F1: cheap AMI AN3PLUS-class
# firmware can't relocate the ISO's signed GRUB from USB, `relocation 0x0`). The mkimage loader is
# UNSIGNED → the target board MUST have Secure Boot OFF (documented in the n100 profile's prep). The
# loader surgery runs AFTER prepare-iso (the assistant's answer/first-boot payload is provably
# untouched except the loader path).
#
# SECRET-BEARING: if the bootstrap-env carries a retrieval passphrase (it must, for an unattended
# install — see README "secret-bearing"), the produced ISO embeds it. Supervised/single-use only;
# never distributed; delete after the run. The build log says so loudly.
#
# Runs on DooPlex; delegates validate-answer + prepare-iso to the felhom-iso-assistant container.
# Runs on DooPlex; delegates validate-answer + prepare-iso + the mkimage surgery to the
# felhom-iso-assistant container (which carries proxmox-auto-install-assistant, xorriso, grub-mkimage,
# and mtools).
#===============================================================================
set -euo pipefail
ISO_VERSION="1.16.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION).
ISO_VERSION="1.18.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION).
IMAGE="${FELHOM_ISO_ASSISTANT_IMAGE:-felhom-iso-assistant:trixie}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -32,6 +43,7 @@ log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; }
die() { log_error "$1"; exit 1; }
PVE_ISO=""; ISO_SHA256=""; PROFILE=""; BOOTSTRAP_ENV=""; OUT_DIR="${HOME}/felhom-iso/out"; PVE_VERSION=""; DRY_RUN=false
LOADER_CLI="" # --loader override; empty = fall back to the profile, then the shim default.
usage() {
cat <<EOF
Usage: build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --bootstrap-env FILE [options]
@@ -43,6 +55,10 @@ Required:
--bootstrap-env FILE the in-ISO /etc/felhom/bootstrap.env (SECRET-BEARING: retrieval passphrase).
Must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE.
Options:
--loader shim|mkimage UEFI boot loader (default: shim, or the profile's FELHOM_LOADER; --loader wins).
shim = stock MS-signed chain (Secure Boot OK on compliant firmware).
mkimage = monolithic grub-mkimage loader for cheap AMI boards that can't boot
the ISO's GRUB from USB (F1). UNSIGNED -> the target board needs Secure Boot OFF.
--out DIR output directory (default: \$HOME/felhom-iso/out)
--pve-version VER override PVE version tag (default: parsed from the ISO filename)
--dry-run print the steps without producing an ISO
@@ -55,6 +71,7 @@ while [[ $# -gt 0 ]]; do
--iso-sha256) ISO_SHA256="$2"; shift 2 ;;
--profile) PROFILE="$2"; shift 2 ;;
--bootstrap-env) BOOTSTRAP_ENV="$2"; shift 2 ;;
--loader) LOADER_CLI="$2"; shift 2 ;;
--out) OUT_DIR="$2"; shift 2 ;;
--pve-version) PVE_VERSION="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
@@ -62,6 +79,8 @@ while [[ $# -gt 0 ]]; do
*) die "unknown argument: $1 (see --help)" ;;
esac
done
[[ -z "$LOADER_CLI" || "$LOADER_CLI" == "shim" || "$LOADER_CLI" == "mkimage" ]] \
|| die "--loader must be 'shim' or 'mkimage' (got '$LOADER_CLI')"
[[ -n "$PVE_ISO" ]] || die "--pve-iso is required"
[[ -n "$ISO_SHA256" ]] || die "--iso-sha256 is required"
@@ -88,7 +107,7 @@ PROFILE_NAME="$(basename "$PROFILE")"; PROFILE_NAME="${PROFILE_NAME%.profile}"
# --- load + validate profile ----------------------------------------------------------------------
log_step "loading profile: $PROFILE"
FELHOM_FQDN=""; FELHOM_DISK_SETUP=""; FELHOM_ROOT_SSH_KEY=""
FELHOM_FQDN=""; FELHOM_DISK_SETUP=""; FELHOM_ROOT_SSH_KEY=""; FELHOM_LOADER=""
# shellcheck disable=SC1090
source "$PROFILE"
[[ -n "$FELHOM_FQDN" ]] || die "profile missing FELHOM_FQDN"
@@ -97,6 +116,20 @@ source "$PROFILE"
ROOT_SSH_LINE=""
[[ -n "$FELHOM_ROOT_SSH_KEY" ]] && ROOT_SSH_LINE="root-ssh-keys = [\"${FELHOM_ROOT_SSH_KEY}\"]"
# --- resolve the loader mode: --loader wins over the profile's FELHOM_LOADER over the shim default ---
LOADER="${LOADER_CLI:-${FELHOM_LOADER:-shim}}"
[[ "$LOADER" == "shim" || "$LOADER" == "mkimage" ]] \
|| die "profile FELHOM_LOADER must be 'shim' or 'mkimage' (got '$FELHOM_LOADER')"
if [[ "$LOADER" == "mkimage" ]]; then
echo -e "${YELLOW}==================================================================================${NC}"
log_warn "LOADER MODE = mkimage — the UEFI boot path is a monolithic grub-mkimage loader (F1 fix)."
log_warn "This loader is UNSIGNED: the target board MUST have Secure Boot OFF. shim/SB is bypassed."
log_warn "The ISO filename gains '-mkimage'; never confuse it with a shim (SB-capable) build."
echo -e "${YELLOW}==================================================================================${NC}"
else
log_info "loader mode = shim (stock MS-signed chain; Secure Boot works on compliant firmware)"
fi
# --- validate bootstrap-env (secret-bearing detection) --------------------------------------------
log_step "checking bootstrap-env (secret-bearing detection)"
( set +e
@@ -112,7 +145,8 @@ log_warn "this ISO will be SECRET-BEARING (embeds the customer retrieval passphr
# --- workspace ------------------------------------------------------------------------------------
WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-iso.XXXXXX")"
cleanup() { rm -rf "$WORK"; }
# chmod first: the mkimage surgery's osirrox extract leaves read-only file modes that rm can't clear.
cleanup() { chmod -R u+w "$WORK" 2>/dev/null || true; rm -rf "$WORK"; }
trap cleanup EXIT
mkdir -p "$OUT_DIR" "$WORK/tmp"
ISO_DIR="$(cd "$(dirname "$PVE_ISO")" && pwd)"; ISO_BASE="$(basename "$PVE_ISO")"
@@ -174,10 +208,14 @@ chmod 0755 "$STUB"
grep -q '@@BOOTSTRAP_.*_B64@@' "$STUB" && die "stub still has unfilled markers — injection failed"
# --- prepare-iso ----------------------------------------------------------------------------------
OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}.iso"
# Rule 4: the loader mode is loud in the filename — a '-mkimage' ISO implies Secure-Boot-off prep.
LOADER_SUFFIX=""; [[ "$LOADER" != "shim" ]] && LOADER_SUFFIX="-${LOADER}"
OUT_ISO="$OUT_DIR/felhom-pve-${PVE_VERSION}-v${ISO_VERSION}-${PROFILE_NAME}${LOADER_SUFFIX}.iso"
GRUB_VERSION="" # populated by the mkimage surgery (the grub-mkimage build used)
log_step "building ISO: $(basename "$OUT_ISO")"
if $DRY_RUN; then
log_dry "docker run … prepare-iso /iso/$ISO_BASE --fetch-from iso --answer-file /work/answer.toml --on-first-boot /work/stub-first-boot.sh --output /work/out.iso"
[[ "$LOADER" == "mkimage" ]] && log_dry "docker run … (mkimage surgery) grub-mkimage from the ISO's own modules → swap BOOTX64.EFI in the EFI tree + efi.img → xorriso re-master → /work/final.iso"
log_info "DRY-RUN: no ISO produced"
exit 0
fi
@@ -187,15 +225,35 @@ docker run --rm -v "$ISO_DIR":/iso:ro -v "$WORK":/work "$IMAGE" \
--on-first-boot /work/stub-first-boot.sh \
--tmp /work/tmp --output /work/out.iso
[[ -f "$WORK/out.iso" ]] || die "prepare-iso produced no output"
cp "$WORK/out.iso" "$OUT_ISO"
# --- SLICE B: mkimage loader surgery (post-prepare; the assistant payload is untouched but the EFI
# boot path). The recipe is the N100 run's proven workaround (VALIDATION F1) — do NOT re-derive it:
# grub-mkimage from the ISO's OWN x86_64-efi modules (2.12-9+pmx2), embedding the module set the
# ISO's grub.cfg needs + an embedded config that `search --fs-uuid`es the ISO and `configfile`s its
# real menu; then swap BOOTX64.EFI in the ISO9660 EFI/BOOT tree AND inside efi.img, and re-master
# with xorriso preserving BOTH the hybrid BIOS boot and the injected answer/first-boot payload. ---
if [[ "$LOADER" == "mkimage" ]]; then
log_step "applying mkimage UEFI loader (F1 firmware fix; recipe from the N100 run evidence)"
[[ -f "$HERE/mkimage-surgery.sh" ]] || die "mkimage-surgery.sh not found next to build-felhom-iso.sh"
cp "$HERE/mkimage-surgery.sh" "$WORK/mkimage-surgery.sh"
docker run --rm -v "$WORK":/work "$IMAGE" bash /work/mkimage-surgery.sh 2>&1 | sed 's/^/ [surgery] /'
[[ -f "$WORK/final.iso" ]] || die "mkimage surgery produced no output (see [surgery] log above)"
GRUB_VERSION="$(cat "$WORK/grub-version.txt" 2>/dev/null || echo unknown)"
cp "$WORK/final.iso" "$OUT_ISO"
log_success "mkimage loader applied (grub-mkimage: ${GRUB_VERSION})"
else
cp "$WORK/out.iso" "$OUT_ISO"
fi
# --- sha256 + manifest ----------------------------------------------------------------------------
OUT_SHA="$(sha256sum "$OUT_ISO" | awk '{print $1}')"
OUT_SIZE="$(stat -c '%s' "$OUT_ISO")"
ASSISTANT_VER="$(docker run --rm "$IMAGE" proxmox-auto-install-assistant --version 2>&1 | head -1)"
echo "$OUT_SHA $(basename "$OUT_ISO")" > "$OUT_ISO.sha256"
LOADER_NOTE="shim (stock MS-signed chain; Secure Boot OK on compliant firmware)"
[[ "$LOADER" == "mkimage" ]] && LOADER_NOTE="mkimage (monolithic grub-mkimage UEFI loader, F1 fix — UNSIGNED; target board MUST have Secure Boot OFF)"
cat > "$OUT_ISO.manifest.txt" <<EOF
Felhom bare-metal ISO build manifest (R-21 slice A)
Felhom bare-metal ISO build manifest (R-21 slice A+B)
built : $(date -Is)
iso-version-tag : v${ISO_VERSION}
pve-version : ${PVE_VERSION}
@@ -204,6 +262,8 @@ source-iso-sha256 : ${ISO_SHA256}
assistant-version : ${ASSISTANT_VER}
profile : ${PROFILE_NAME}
fqdn : ${FELHOM_FQDN}
loader : ${LOADER_NOTE}
grub-mkimage : ${GRUB_VERSION:-n/a (shim mode; loader unchanged)}
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} (embeds the customer retrieval passphrase — supervised/single-use, delete after the run)
output : $(basename "$OUT_ISO")