#!/bin/bash #=============================================================================== # build-felhom-iso.sh — R-21 slice A+B+C: turn the official PVE ISO into a Felhom auto-install ISO. # # SLICE C — --pairing builds the GENERIC, SECRET-FREE universal ISO: no customer-id / passphrase is # baked in. The box registers itself at the hub as an UNCLAIMED APPLIANCE, the operator binds it to a # customer, and the hub delivers the credentials ONCE — then the box completes day-0 exactly like a # direct-mode box. Direct mode (--bootstrap-env, secret-bearing, operator-prepped) is unchanged. # # 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 # exit 0 even on failure, spike S1 trap), renders the first-boot stub (injecting the bootstrap # script/unit/env), and runs prepare-iso --fetch-from iso --on-first-boot. Emits the ISO + sha256 + # a build manifest. # # R-38 GRUB SLICE (v1.22.0) — every ISO is now REPACKED after prepare-iso to carry the Felhom boot # screen and, more importantly, a menu with exactly ONE entry ("Felhom telepítés", default, 5s). The # stock PVE menu offers Graphical/Terminal-UI/serial installers plus an Advanced Options submenu with # debug/nomodeset/rescue entries — every one of them a route into the MANUAL installer, whose first # question is which disk to wipe. A customer, or their helpful nephew, must not be able to reach it # from a boot menu; those entries are not hidden, they are not emitted. The kernel/append line is # lifted verbatim from the ISO's own automated entry, so boot behavior is unchanged. See iso-repack.sh. # (--no-brand exists for debugging the stock menu; it is not a shipping mode.) # # 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 + 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.25.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)" # --- logging (host-install idiom) ----------------------------------------------------------------- if [[ -t 1 ]]; then RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; BLUE=$'\033[0;34m'; CYAN=$'\033[0;36m'; NC=$'\033[0m' else RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""; NC=""; fi log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } log_step() { echo -e "${BLUE}[STEP]${NC} $1"; } log_success() { echo -e "${GREEN}[OK]${NC} $1"; } log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; } die() { log_error "$1"; exit 1; } # ver_ge A B → true iff version A >= version B (dotted numeric, sort -V). ver_ge() { [[ "$1" == "$2" ]] && return 0; [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" == "$1" ]]; } # R-71 STANDING GATE (v1.25.0): every ISO build asserts the golden it will install is >= the managed # controller floor. If the floor exceeds golden, a freshly-installed box boots BELOW the floor and the # day-0 managed update fires — racing the offsite apply-bridge exactly as it did on demo-hp # (DIAG-f10-demo-hp-offsite-2026-07-23 / R-71). The build must fail loudly rather than ship that gap. # Inputs: FELHOM_ASSERT_GOLDEN + FELHOM_ASSERT_FLOOR (the hub's current artifact_golden_version and # min_controller_version — resolve them operator-side and pass in; the publish-train runbook covers # how). Both printed. If either is unset the gate is UNENFORCED and says so LOUDLY (never a silent pass). assert_golden_ge_floor() { local golden="${FELHOM_ASSERT_GOLDEN:-}" floor="${FELHOM_ASSERT_FLOOR:-}" if [[ -z "$golden" || -z "$floor" ]]; then log_warn "R-71 golden>=floor gate UNENFORCED — pass FELHOM_ASSERT_GOLDEN + FELHOM_ASSERT_FLOOR to enforce (golden='${golden:-unset}' floor='${floor:-unset}')" return 0 fi if ver_ge "$golden" "$floor"; then log_success "R-71 gate OK: golden $golden >= managed floor $floor" else die "R-71 BUILD GATE FAILED: golden $golden < managed floor $floor — a fresh box would boot below the floor and the day-0 update would race the offsite apply-bridge. Republish golden >= $floor (and vouch it) before building this ISO." fi } 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. 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 < the target board needs Secure Boot OFF. --no-brand DEBUG ONLY: keep the stock PVE boot menu (all installer entries reachable). Shipping ISOs are always branded: Felhom boot screen + exactly ONE entry. --brand-image FILE override the boot-screen card (default: website/assets/og-image_2.png) --out DIR output directory (default: the DooPlex build root /mnt/5_hdd/felhom.eu/felhom-iso/out; override via \$FELHOM_ISO_OUT for other hosts) --pve-version VER override PVE version tag (default: parsed from the ISO filename) --dry-run print the steps without producing an ISO -h, --help this help EOF } while [[ $# -gt 0 ]]; do case "$1" in --pve-iso) PVE_ISO="$2"; shift 2 ;; --iso-sha256) ISO_SHA256="$2"; shift 2 ;; --profile) PROFILE="$2"; shift 2 ;; --bootstrap-env) BOOTSTRAP_ENV="$2"; shift 2 ;; --pairing) PAIRING=true; shift ;; --loader) LOADER_CLI="$2"; shift 2 ;; --no-brand) BRAND=false; shift ;; --brand-image) BRAND_IMAGE="$2"; shift 2 ;; --out) OUT_DIR="$2"; shift 2 ;; --pve-version) PVE_VERSION="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; -h|--help) usage; exit 0 ;; *) 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" [[ -n "$PROFILE" ]] || die "--profile is required" [[ -f "$PVE_ISO" ]] || die "--pve-iso not found: $PVE_ISO" [[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE" # 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" else [[ -n "$BOOTSTRAP_ENV" ]] || die "one of --bootstrap-env (direct) or --pairing (generic) is required" [[ -f "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env not found: $BOOTSTRAP_ENV" fi command -v docker >/dev/null || die "docker not found (needed for the assistant container)" docker image inspect "$IMAGE" >/dev/null 2>&1 || die "assistant image '$IMAGE' not found — build it: docker build -f $HERE/Dockerfile.assistant -t $IMAGE $HERE" # --- verify source ISO ---------------------------------------------------------------------------- log_step "verifying source ISO sha256" actual_sha=$(sha256sum "$PVE_ISO" | awk '{print $1}') [[ "$actual_sha" == "$ISO_SHA256" ]] || die "ISO sha256 MISMATCH: expected $ISO_SHA256, got $actual_sha" log_success "source ISO sha256 OK ($actual_sha)" if [[ -z "$PVE_VERSION" ]]; then PVE_VERSION=$(basename "$PVE_ISO" | sed -E 's/^proxmox-ve_(.+)\.iso$/\1/') [[ "$PVE_VERSION" != "$(basename "$PVE_ISO")" ]] || die "cannot parse PVE version from '$(basename "$PVE_ISO")' — pass --pve-version" fi 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_LOADER=""; FELHOM_HUB_URL=""; FELHOM_INSTALL_URL="" # shellcheck disable=SC1090 source "$PROFILE" [[ -n "$FELHOM_FQDN" ]] || die "profile missing FELHOM_FQDN" [[ -n "$FELHOM_DISK_SETUP" ]] || die "profile missing FELHOM_DISK_SETUP" # 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}\"]" # --- 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 # --- resolve the boot-screen card (R-38) ------------------------------------------------------------ # Single source of truth: the website asset. NOT copied into scripts/iso/ — a second copy of a brand # asset is a second thing to forget to update. if $BRAND; then [[ -n "$BRAND_IMAGE" ]] || BRAND_IMAGE="$HERE/../../website/assets/og-image_2.png" [[ -f "$BRAND_IMAGE" ]] || die "brand image not found: $BRAND_IMAGE (pass --brand-image, or run from the repo checkout)" log_info "boot-screen card = $BRAND_IMAGE (menu: exactly ONE entry, 'Felhom telepítés')" else echo -e "${YELLOW}==================================================================================${NC}" log_warn "--no-brand — the STOCK PVE boot menu ships: Graphical/Terminal-UI/serial installers and" log_warn "the Advanced Options submenu are all reachable, i.e. a manual 'which disk do I wipe'" log_warn "installer is one keypress away. DEBUGGING ONLY — never hand this ISO to a customer." echo -e "${YELLOW}==================================================================================${NC}" fi # --- mode: DIRECT validates the secret-bearing env; PAIRING is secret-free (env generated below) ----- if $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}" echo -e "${YELLOW}==================================================================================${NC}" log_info "PAIRING MODE — building the GENERIC, SECRET-FREE universal ISO (slice C)." log_info "The box registers as an unclaimed appliance; the operator binds it; the hub delivers the" log_info "customer-id + passphrase ONCE. Baked env carries only the hub URL ($PAIR_HUB_URL) — no secret." echo -e "${YELLOW}==================================================================================${NC}" else log_step "checking bootstrap-env (secret-bearing detection)" ( set +e FELHOM_CUSTOMER_ID=""; FELHOM_MODE=""; FELHOM_RETRIEVAL_PASSPHRASE="" # shellcheck disable=SC1090 source "$BOOTSTRAP_ENV" [[ -n "$FELHOM_CUSTOMER_ID" ]] || { echo "MISSING FELHOM_CUSTOMER_ID"; exit 3; } [[ -n "$FELHOM_MODE" ]] || { echo "MISSING FELHOM_MODE"; exit 3; } [[ -n "$FELHOM_RETRIEVAL_PASSPHRASE" ]] || { echo "MISSING FELHOM_RETRIEVAL_PASSPHRASE"; exit 3; } ) || die "bootstrap-env invalid ($BOOTSTRAP_ENV) — must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE" SECRET_BEARING="yes" # a valid bootstrap-env always carries the retrieval passphrase log_warn "this ISO will be SECRET-BEARING (embeds the customer retrieval passphrase) — supervised/single-use only" fi # R-71 build gate — before any workspace/mint/build work commits (fails fast + loud). assert_golden_ge_floor # --- workspace ------------------------------------------------------------------------------------ WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-iso.XXXXXX")" # chmod first: the mkimage surgery's osirrox extract leaves read-only file modes that rm can't clear. # FELHOM_ISO_KEEP_WORK=1 is a DEBUG/TEST escape: keep $WORK for post-mortem (the rootpw-emission # harness cross-checks the emitted plaintext against the rendered answer's hash). Never a build mode. cleanup() { if [[ "${FELHOM_ISO_KEEP_WORK:-0}" == "1" ]]; then log_warn "FELHOM_ISO_KEEP_WORK=1 — workspace KEPT at $WORK (debug/test only; delete it yourself)" return 0 fi 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")" # PAIRING: generate the SECRET-FREE env the stub bakes — only the hub URL, no customer/passphrase. # (The bootstrap detects the absent customer-id/passphrase and enters pairing mode.) if $PAIRING; then BOOTSTRAP_ENV="$WORK/pairing.env" cat > "$BOOTSTRAP_ENV" < "$OUT_ISO.rootpw.txt" ) 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)" # --- render answer.toml (pure bash param-expansion; no metachar hazards) --------------------------- log_step "rendering answer.toml" ANSWER="$WORK/answer.toml" : > "$ANSWER" while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" == "__DISK_SETUP__" ]]; then printf '%s\n' "$FELHOM_DISK_SETUP" >> "$ANSWER" elif [[ "$line" == "__ROOT_SSH_KEYS__" ]]; then [[ -n "$ROOT_SSH_LINE" ]] && printf '%s\n' "$ROOT_SSH_LINE" >> "$ANSWER" # blank -> omit line else line="${line//__FQDN__/$FELHOM_FQDN}" line="${line//__ROOT_HASH__/$ROOT_HASH}" printf '%s\n' "$line" >> "$ANSWER" fi done < "$HERE/answer.toml.tmpl" # --- validate-answer OUTPUT-PARSE gate (never $? — spike S1) -------------------------------------- gate_validate_answer() { local out out=$(docker run --rm -v "$WORK":/work "$IMAGE" \ proxmox-auto-install-assistant validate-answer /work/answer.toml 2>&1) || true echo "----- validate-answer output -----"; echo "$out"; echo "----------------------------------" # LOAD-BEARING: validate-answer exits 0 even on failure; decide on the MESSAGE TEXT, not $?. if echo "$out" | grep -q "parsed successfully" && ! echo "$out" | grep -qi "Found issues"; then return 0 fi return 1 } log_step "validating rendered answer (output-parse gate)" if $DRY_RUN; then log_dry "docker run … validate-answer /work/answer.toml (output-parse gate)" else gate_validate_answer || die "answer validation FAILED — NO ISO produced (fix the answer/profile)" log_success "answer validated" fi # --- render the first-boot stub (inject bootstrap script/unit/env as base64) ---------------------- log_step "rendering first-boot stub" STUB="$WORK/stub-first-boot.sh" sh_b64="$(base64 -w0 < "$HERE/felhom-bootstrap.sh")" unit_b64="$(base64 -w0 < "$HERE/felhom-bootstrap.service")" env_b64="$(base64 -w0 < "$BOOTSTRAP_ENV")" awk -v sh="$sh_b64" -v unit="$unit_b64" -v env="$env_b64" ' { gsub(/@@BOOTSTRAP_SH_B64@@/, sh); gsub(/@@BOOTSTRAP_UNIT_B64@@/, unit); gsub(/@@BOOTSTRAP_ENV_B64@@/, env); print } ' "$HERE/stub-first-boot.sh" > "$STUB" chmod 0755 "$STUB" grep -q '@@BOOTSTRAP_.*_B64@@' "$STUB" && die "stub still has unfilled markers — injection failed" # --- prepare-iso ---------------------------------------------------------------------------------- # (OUT_ISO is resolved above the mint — the rootpw sibling file is named after it.) 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" $BRAND && log_dry "docker run … (repack/brand) generate 1024x768 boot card from $(basename "$BRAND_IMAGE") → install felhomtheme → rewrite grub.cfg to ONE entry ('Felhom telepítés', 5s), kernel line lifted from the stock automated entry" [[ "$LOADER" == "mkimage" ]] && log_dry "docker run … (repack/mkimage) grub-mkimage from the ISO's own modules → swap BOOTX64.EFI in the EFI tree + efi.img" ( $BRAND || [[ "$LOADER" == "mkimage" ]] ) && log_dry "docker run … (repack) xorriso re-master → /work/final.iso" 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 [[ -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 # need the same extract -> modify -> re-master cycle, so iso-repack.sh does them in ONE pass. The # assistant's answer/first-boot payload is untouched; only the GRUB menu/theme and (mkimage) the # EFI boot path change. The mkimage recipe is the N100 run's proven workaround (VALIDATION F1) — # do NOT re-derive it. See iso-repack.sh for the full rationale. ----------------------------------- if $BRAND || [[ "$LOADER" == "mkimage" ]]; then REPACK_WHAT="" $BRAND && REPACK_WHAT="branding" [[ "$LOADER" == "mkimage" ]] && REPACK_WHAT="${REPACK_WHAT:+$REPACK_WHAT + }mkimage loader" log_step "repacking ISO ($REPACK_WHAT)" [[ -f "$HERE/iso-repack.sh" ]] || die "iso-repack.sh not found next to build-felhom-iso.sh" cp "$HERE/iso-repack.sh" "$WORK/iso-repack.sh" if $BRAND; then mkdir -p "$WORK/brand" cp "$HERE/grub/grub.cfg.tmpl" "$HERE/grub/felhom-theme.txt" \ "$HERE/grub/generate-grub-background.sh" "$WORK/brand/" cp "$BRAND_IMAGE" "$WORK/brand/card.png" fi docker run --rm -v "$WORK":/work \ -e FELHOM_LOADER="$LOADER" -e FELHOM_BRAND="$($BRAND && echo 1 || echo 0)" \ "$IMAGE" bash /work/iso-repack.sh 2>&1 | sed 's/^/ [repack] /' [[ -f "$WORK/final.iso" ]] || die "repack produced no output (see [repack] log above)" GRUB_VERSION="$(cat "$WORK/grub-version.txt" 2>/dev/null || echo unknown)" cp "$WORK/final.iso" "$OUT_ISO" log_success "repack applied ($REPACK_WHAT)" 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)" BRAND_NOTE="FELHOM (single entry 'Felhom telepítés', default, 5s; stock installer entries not emitted)" $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)" cat > "$OUT_ISO.manifest.txt" </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) output : $(basename "$OUT_ISO") output-sha256 : ${OUT_SHA} output-size-bytes : ${OUT_SIZE} EOF log_success "ISO built: $OUT_ISO" 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 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." fi