hub v0.62.0 + scripts v1.19.0 — R-21 slice C: the universal secret-free ISO

A generic ISO carries NO customer secret. The box registers itself at the hub
as an unclaimed appliance; the operator binds it to a customer; the hub delivers
the customer-id + retrieval passphrase ONCE; day-0 completes via the slice-A path.

Hub (v0.62.0):
- store/appliance.go: appliance_registrations keyed by (uuid, mac_set) — MAC set
  is the tiebreaker (duplicate SMBIOS UUIDs); token stored as sha256 only.
  Idempotent register (sticky-discard), atomic one-shot delivery, bind/discard.
- api/appliance.go: POST /appliance/register (the one unauth endpoint, per-IP
  rate-limited, 256-bit token); GET /appliance/poll (404 no-oracle / 204 unbound
  / 200 deliver-once / 410 delivered). Passphrase read live, never logged.
- web/appliances.go: Hosts-page "Unclaimed appliances" section + BIND (customer
  picker, host count display-only) + DISCARD; SSH host-key fingerprints; events.
- Red-proofs: one-shot delivery + register idempotency (both proven red);
  404-no-oracle, sticky-discard, bind staging, render. Green + confirm gate.

Scripts (v1.19.0):
- felhom-bootstrap.sh: ONE unit, TWO modes. Direct (env has customer/passphrase)
  = slice-A path, byte-identical, only branched around. Pairing (generic) =
  register + poll (RestartSec=30 is the poll timer); on delivery write the env
  0600 and fall through to direct. Secrets + token shredded on success.
- build-felhom-iso.sh --pairing: generic secret-free ISO, -generic filename,
  manifest mode=pairing. profiles/generic.profile (new).
- test/bootstrap-modes.sh: Scenario D (direct = zero appliance calls) + pairing
  register/poll + delivery handoff — all green in a debian container.
This commit is contained in:
2026-07-17 15:07:31 +02:00
parent 3172df1927
commit 36c5cd5fdf
16 changed files with 1531 additions and 90 deletions
+21
View File
@@ -1,5 +1,26 @@
# Felhom scripts — Changelog
## build-felhom-iso.sh v1.19.0 — the universal secret-free ISO: `--pairing` mode (R-21 slice C) (2026-07-17)
The scripts half of the universal ISO. `felhom-bootstrap.sh` gains a PAIRING mode — **one unit, two
modes**, decided by the env:
- **DIRECT** (env has `FELHOM_CUSTOMER_ID` + `FELHOM_RETRIEVAL_PASSPHRASE`): the slice-A path,
**byte-identical** — only branched around. Scenario D regression proves the pairing code is provably
NOT entered (a fake hub records ZERO `/appliance/*` calls).
- **PAIRING** (generic ISO, no customer/passphrase baked in): gather identity (SMBIOS uuid + physical
MAC set + SSH host pubkeys + hw), `POST /api/v1/appliance/register` → persist the appliance token
(0600), then ONE `GET /api/v1/appliance/poll` per invocation (the existing
`Restart=on-failure`/`RestartSec=30` IS the poll timer — no long-running-oneshot timeout). On the
bind's 200 delivery, WRITE the delivered credentials into the env (0600) and fall through to the
DIRECT path — so every later retry is a plain direct install (the delivery is one-shot; a second poll
→ 410). Delivery-received secrets + the appliance token are shredded on host-install success.
- **`build-felhom-iso.sh --pairing`** builds the GENERIC ISO: no `--bootstrap-env`, a secret-free env
carrying only the hub URL, `-generic` filename marker, manifest `mode: pairing` + `secret-bearing:
no`. Direct mode (secret-bearing) is unchanged. **New `profiles/generic.profile`.**
- Validated: `bash -n` + shellcheck clean; the `test/bootstrap-modes.sh` harness (Scenario D + pairing
register/poll + the delivery→env→host-install handoff) all green in a debian container.
## build-felhom-iso.sh v1.18.0 — firmware loader option `--loader shim|mkimage` (R-21 slice B, F1) (2026-07-17)
Closes N100 finding **F1 (HIGH):** cheap AMI (`AN3PLUS 0.01`-class) UEFI firmware can't relocate the
+75 -24
View File
@@ -1,6 +1,11 @@
#!/bin/bash
#===============================================================================
# build-felhom-iso.sh — R-21 slice A+B: turn the official PVE ISO into a Felhom auto-install ISO.
# 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
@@ -27,7 +32,7 @@
#===============================================================================
set -euo pipefail
ISO_VERSION="1.18.0" # Felhom release the ISO is tagged to (aligns with felhom-host-install SCRIPT_VERSION).
ISO_VERSION="1.19.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)"
@@ -44,16 +49,24 @@ 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.
PAIRING=false # --pairing: build the GENERIC secret-free ISO (slice C); no --bootstrap-env.
usage() {
cat <<EOF
Usage: build-felhom-iso.sh --pve-iso PATH --iso-sha256 SHA --profile FILE --bootstrap-env FILE [options]
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]
Required:
--pve-iso PATH pre-downloaded official PVE ISO (not fetched here)
--iso-sha256 SHA expected sha256 of --pve-iso (verified before build; abort on mismatch)
--profile FILE build profile (fqdn + [disk-setup]); see profiles/ and README
--bootstrap-env FILE the in-ISO /etc/felhom/bootstrap.env (SECRET-BEARING: retrieval passphrase).
Must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE.
Mode (exactly one):
--bootstrap-env FILE DIRECT mode: the in-ISO /etc/felhom/bootstrap.env (SECRET-BEARING: retrieval
passphrase). Must define FELHOM_CUSTOMER_ID, FELHOM_MODE, FELHOM_RETRIEVAL_PASSPHRASE.
--pairing PAIRING mode (slice C): the GENERIC, SECRET-FREE universal ISO. The box registers
itself as an unclaimed appliance at the hub; the operator binds it; the hub
delivers the customer-id + passphrase ONCE. No customer secret is baked in. The
hub URL comes from the profile (FELHOM_HUB_URL) or the default.
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).
@@ -71,6 +84,7 @@ while [[ $# -gt 0 ]]; do
--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 ;;
--out) OUT_DIR="$2"; shift 2 ;;
--pve-version) PVE_VERSION="$2"; shift 2 ;;
@@ -85,10 +99,15 @@ done
[[ -n "$PVE_ISO" ]] || die "--pve-iso is required"
[[ -n "$ISO_SHA256" ]] || die "--iso-sha256 is required"
[[ -n "$PROFILE" ]] || die "--profile is required"
[[ -n "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env is required"
[[ -f "$PVE_ISO" ]] || die "--pve-iso not found: $PVE_ISO"
[[ -f "$PROFILE" ]] || die "--profile not found: $PROFILE"
[[ -f "$BOOTSTRAP_ENV" ]] || die "--bootstrap-env not found: $BOOTSTRAP_ENV"
# 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"
@@ -107,7 +126,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_LOADER=""
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"
@@ -130,18 +149,29 @@ 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
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"
# --- 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
# --- workspace ------------------------------------------------------------------------------------
WORK="$(mktemp -d "${TMPDIR:-/tmp}/felhom-iso.XXXXXX")"
@@ -151,6 +181,19 @@ 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" <<EOF
# GENERIC secret-free pairing env (R-21 slice C). NO customer-id, NO passphrase — the box registers
# as an unclaimed appliance and the hub delivers the credentials once, after the operator binds it.
FELHOM_HUB_URL=$PAIR_HUB_URL
FELHOM_INSTALL_URL=$PAIR_INSTALL_URL
EOF
log_info "generated secret-free pairing env (hub=$PAIR_HUB_URL)"
fi
# --- mint fresh THROWAWAY root hash ---------------------------------------------------------------
log_step "minting fresh throwaway root password hash"
ROOT_PLAIN="felhom-throwaway-$(head -c12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9')"
@@ -210,7 +253,8 @@ grep -q '@@BOOTSTRAP_.*_B64@@' "$STUB" && die "stub still has unfilled markers
# --- prepare-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"
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"
GRUB_VERSION="" # populated by the mkimage surgery (the grub-mkimage build used)
log_step "building ISO: $(basename "$OUT_ISO")"
if $DRY_RUN; then
@@ -252,8 +296,10 @@ ASSISTANT_VER="$(docker run --rm "$IMAGE" proxmox-auto-install-assistant --versi
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)"
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" <<EOF
Felhom bare-metal ISO build manifest (R-21 slice A+B)
Felhom bare-metal ISO build manifest (R-21 slice A+B+C)
built : $(date -Is)
iso-version-tag : v${ISO_VERSION}
pve-version : ${PVE_VERSION}
@@ -262,10 +308,11 @@ source-iso-sha256 : ${ISO_SHA256}
assistant-version : ${ASSISTANT_VER}
profile : ${PROFILE_NAME}
fqdn : ${FELHOM_FQDN}
mode : ${MODE_NOTE}
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)
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)')
output : $(basename "$OUT_ISO")
output-sha256 : ${OUT_SHA}
output-size-bytes : ${OUT_SIZE}
@@ -275,4 +322,8 @@ 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_warn "SECRET-BEARING ISO (embeds the retrieval passphrase). Supervised/single-use; never distribute; delete after the run."
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
+212 -65
View File
@@ -2,23 +2,29 @@
#===============================================================================
# felhom-bootstrap.sh — invoked by felhom-bootstrap.service, retried until host-install succeeds.
#
# One attempt: read /etc/felhom/bootstrap.env -> fetch felhom-host-install.sh from the PUBLIC
# distribution channel (hub install-command Option-1 URL) -> run it unattended with the customer's
# retrieval passphrase -> on rc 0 write the done-flag + disable the unit; else exit non-zero so the
# unit retries. Journal-only logging; the passphrase is never echoed and lives only in a 0600 tmpfs
# file for the duration of one host-install invocation.
# ONE unit, TWO modes, decided by the env:
# DIRECT (env has FELHOM_CUSTOMER_ID + FELHOM_RETRIEVAL_PASSPHRASE) — the slice-A path, unchanged:
# fetch felhom-host-install.sh from the PUBLIC channel -> run it unattended with the
# customer's retrieval passphrase -> on rc 0 write the done-flag + disable + shred the env.
# PAIRING (R-21 slice C — the GENERIC secret-free ISO, no customer-id/passphrase in the env):
# register this box as an UNCLAIMED appliance at the hub (uuid + MAC set + SSH host keys +
# hw), receive a one-per-registration APPLIANCE TOKEN (0600), then POLL for the operator's
# bind. ONE delivery hands over customer-id + retrieval passphrase; the bootstrap WRITES
# them into the env (0600) and FALLS THROUGH to the DIRECT path — so every later retry is a
# plain direct install (the delivery is one-shot; the box must not depend on re-fetching it).
#
# Retry-vs-resume (source-verified, encoded ONCE): felhom-host-install.sh v1.11.3 makes --resume
# safe — its producer steps (token/enroll/grows) re-run every pass, so a resumed install repopulates
# hub.host_id/proxmox.token and never writes a crash-loop config. A plain re-invoke over an existing
# install state, by contrast, would re-hit the populated-host leaf guard / existing-vmid refusal.
# Therefore: FIRST attempt is plain; any later attempt that finds the install state file adds
# --resume. (--mode is required in both forms.) State file: /var/lib/felhom-install/state.json.
# The poll loop IS systemd's Restart=on-failure/RestartSec=30: each invocation does register-if-needed
# + exactly ONE poll, exiting non-zero (retry in 30s) until the bind delivers. This keeps every
# invocation short (no long-running-oneshot timeout) and reuses the existing retry machinery.
#
# Retry-vs-resume (source-verified, encoded ONCE): felhom-host-install.sh v1.11.3 makes --resume safe
# — its producer steps re-run every pass. FIRST direct attempt is plain; any later attempt that finds
# the install state file adds --resume. State file: /var/lib/felhom-install/state.json.
#
# NOT production-generic: this is the R-21 bare-metal first-boot bootstrap. It does NOT modify
# felhom-host-install.sh; it only invokes it.
#===============================================================================
# Deliberately NOT `set -e`: we must capture host-install's exit code and exit on our own terms.
# Deliberately NOT `set -e`: we must capture exit codes and exit on our own terms.
set -uo pipefail
ENV_FILE=/etc/felhom/bootstrap.env
@@ -26,6 +32,7 @@ DONE_FLAG=/etc/felhom/.bootstrap-done
STATE_FILE=/var/lib/felhom-install/state.json
PASS_FILE=/run/felhom-bootstrap-pass
SCRIPT_TMP=/run/felhom-host-install.sh
TOKEN_FILE=/etc/felhom/appliance-token # PAIRING: the box's only pre-day-0 credential (0600, persists reboots)
log() { echo "felhom-bootstrap: $*"; }
@@ -38,61 +45,201 @@ if [[ -e "$DONE_FLAG" ]]; then
exit 0
fi
# --- env ------------------------------------------------------------------------------------------
if [[ ! -r "$ENV_FILE" ]]; then
log "ERROR: $ENV_FILE missing or unreadable — cannot bootstrap (no guessed defaults)"
exit 1
# --- env (may be absent in the generic ISO; a non-secret pairing env can still set FELHOM_HUB_URL) --
FELHOM_CUSTOMER_ID=""; FELHOM_MODE=""; FELHOM_RETRIEVAL_PASSPHRASE=""
FELHOM_HUB_URL=""; FELHOM_INSTALL_URL=""; FELHOM_EXTRA_ARGS=""
if [[ -r "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
source "$ENV_FILE"
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
for var in FELHOM_CUSTOMER_ID FELHOM_MODE FELHOM_RETRIEVAL_PASSPHRASE; do
if [[ -z "${!var:-}" ]]; then
log "ERROR: $var is unset/empty in $ENV_FILE — refusing to guess"
exit 1
fi
done
HUB_URL="${FELHOM_HUB_URL:-https://hub.felhom.eu}"
INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}"
EXTRA_ARGS="${FELHOM_EXTRA_ARGS:-}"
# --- fetch host-install (public channel) ----------------------------------------------------------
log "fetching host-install: $INSTALL_URL"
if ! curl -fsSL --max-time 60 "$INSTALL_URL" -o "$SCRIPT_TMP"; then
log "ERROR: host-install fetch failed (no network yet?) — unit will retry"
exit 1
# =====================================================================================================
# DIRECT mode — fetch + run host-install with the customer passphrase (slice A, unchanged behaviour).
# =====================================================================================================
run_direct() {
for var in FELHOM_CUSTOMER_ID FELHOM_MODE FELHOM_RETRIEVAL_PASSPHRASE; do
if [[ -z "${!var:-}" ]]; then
log "ERROR: $var is unset/empty (direct mode) — refusing to guess"
exit 1
fi
done
log "fetching host-install: $INSTALL_URL"
if ! curl -fsSL --max-time 60 "$INSTALL_URL" -o "$SCRIPT_TMP"; then
log "ERROR: host-install fetch failed (no network yet?) — unit will retry"
exit 1
fi
if [[ ! -s "$SCRIPT_TMP" ]]; then
log "ERROR: fetched host-install is empty — unit will retry"
exit 1
fi
( umask 077; printf '%s' "$FELHOM_RETRIEVAL_PASSPHRASE" > "$PASS_FILE" )
local args=(--customer-id "$FELHOM_CUSTOMER_ID" --mode "$FELHOM_MODE" --hub-url "$HUB_URL" --passphrase-file "$PASS_FILE")
if [[ -f "$STATE_FILE" ]]; then
log "prior install state present ($STATE_FILE) -> adding --resume (host-install v1.11.3: producers re-run, safe)"
args+=(--resume)
fi
local extra
read -ra extra <<< "${FELHOM_EXTRA_ARGS:-}"
log "running host-install (customer=${FELHOM_CUSTOMER_ID} mode=${FELHOM_MODE} hub=${HUB_URL})"
bash "$SCRIPT_TMP" "${args[@]}" "${extra[@]}"
local rc=$?
cleanup_pass
if [[ $rc -eq 0 ]]; then
log "host-install SUCCESS — writing done-flag, disabling unit, scrubbing secrets"
install -d -m 0755 "$(dirname "$DONE_FLAG")"
: > "$DONE_FLAG"; chmod 0644 "$DONE_FLAG"
systemctl disable felhom-bootstrap.service 2>/dev/null || true
# Reduce secret-at-rest: the box is enrolled; the passphrase (and the appliance token) are done.
shred -u "$ENV_FILE" 2>/dev/null || rm -f "$ENV_FILE"
[[ -e "$TOKEN_FILE" ]] && { shred -u "$TOKEN_FILE" 2>/dev/null || rm -f "$TOKEN_FILE"; }
exit 0
fi
log "host-install FAILED rc=${rc} — unit will retry in 30s"
exit "$rc"
}
# =====================================================================================================
# PAIRING mode — register the unclaimed appliance, then ONE poll per invocation until the bind delivers.
# =====================================================================================================
# gather_identity_json builds the registration payload. Keying is (SMBIOS UUID, MAC set) — the N100 DMI
# verdict is that serials are unusable ("Default string"), so only the uuid + physical MAC set are
# trusted; hw is a non-keyed summary. python3 ships with PVE and JSON-encodes robustly.
gather_identity_json() {
local uuid; uuid=$(tr -d '\n' < /sys/class/dmi/id/product_uuid 2>/dev/null)
local product; product=$(tr -d '\n' < /sys/class/dmi/id/product_name 2>/dev/null)
local mem_kb; mem_kb=$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null)
local cpu; cpu=$(awk -F: '/model name/{print $2; exit}' /proc/cpuinfo 2>/dev/null | sed 's/^ *//')
local macs=()
local d n m
for d in /sys/class/net/*; do
n=$(basename "$d")
[[ "$n" == "lo" ]] && continue
[[ -e "$d/device" ]] || continue # physical NICs only (skip bridges/veth/wg)
m=$(cat "$d/address" 2>/dev/null)
[[ -n "$m" && "$m" != "00:00:00:00:00:00" ]] && macs+=("$m")
done
local keys=()
local f
for f in /etc/ssh/ssh_host_*_key.pub; do
[[ -f "$f" ]] && keys+=("$(cat "$f")")
done
UUID_G="$uuid" PRODUCT_G="$product" CPU_G="$cpu" MEM_G="$mem_kb" \
MACS_G="$(printf '%s\n' "${macs[@]}")" KEYS_G="$(printf '%s\n' "${keys[@]}")" \
python3 - <<'PY'
import json, os
def lines(v): return [x for x in (v or "").splitlines() if x.strip()]
print(json.dumps({
"uuid": os.environ.get("UUID_G",""),
"macs": lines(os.environ.get("MACS_G","")),
"ssh_host_pubkeys": lines(os.environ.get("KEYS_G","")),
"hw": {"product": os.environ.get("PRODUCT_G",""),
"cpu": os.environ.get("CPU_G",""),
"mem_kb": int(os.environ.get("MEM_G") or 0)},
}))
PY
}
run_pairing() {
log "PAIRING mode (generic ISO, no baked customer/passphrase) — hub=$HUB_URL"
# 1. register once (persist the token). A present token means we already registered — go poll.
if [[ ! -s "$TOKEN_FILE" ]]; then
local payload; payload=$(gather_identity_json)
if [[ -z "$payload" || "$payload" != *'"uuid"'* ]]; then
log "ERROR: could not gather appliance identity — unit will retry"
exit 1
fi
log "registering unclaimed appliance at the hub"
local resp; resp=$(curl -fsS --max-time 30 -X POST \
-H 'Content-Type: application/json' --data "$payload" \
"$HUB_URL/api/v1/appliance/register" 2>/dev/null)
if [[ $? -ne 0 || -z "$resp" ]]; then
log "ERROR: registration failed (no network yet?) — unit will retry"
exit 1
fi
local token; token=$(printf '%s' "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("appliance_token",""))' 2>/dev/null)
if [[ -z "$token" ]]; then
log "ERROR: registration returned no appliance token — unit will retry"
exit 1
fi
( umask 077; printf '%s' "$token" > "$TOKEN_FILE" )
log "registered — appliance token stored (0600); waiting for the operator to bind this box"
fi
# 2. ONE poll. RestartSec=30 is the poll interval.
local token; token=$(cat "$TOKEN_FILE")
local body code
body=$(curl -sS --max-time 30 -o - -w '\n%{http_code}' \
-H "Authorization: Bearer $token" "$HUB_URL/api/v1/appliance/poll" 2>/dev/null)
code="${body##*$'\n'}"
body="${body%$'\n'*}"
case "$code" in
200)
log "bind DELIVERED — writing credentials to the env and switching to direct install"
# Parse the one-shot delivery into shell-safe env assignments (never echo the passphrase).
local envtext
envtext=$(printf '%s' "$body" | python3 -c '
import json, sys, shlex
d = json.load(sys.stdin)
def emit(k, v): print("%s=%s" % (k, shlex.quote(v or "")))
emit("FELHOM_CUSTOMER_ID", d.get("customer_id"))
emit("FELHOM_RETRIEVAL_PASSPHRASE", d.get("retrieval_passphrase"))
emit("FELHOM_MODE", d.get("mode") or "appliance")
emit("FELHOM_EXTRA_ARGS", d.get("extra_args"))
')
if [[ -z "$envtext" || "$envtext" != *FELHOM_RETRIEVAL_PASSPHRASE=* ]]; then
log "ERROR: delivery parse failed — unit will retry"
exit 1
fi
# Persist as the direct-mode env (0600) so EVERY later retry is a plain direct install
# (the delivery was one-shot; a second poll returns 410).
install -d -m 0755 "$(dirname "$ENV_FILE")"
( umask 077
{ printf '%s\n' "$envtext"
printf 'FELHOM_HUB_URL=%q\n' "$HUB_URL"
printf 'FELHOM_INSTALL_URL=%q\n' "$INSTALL_URL"
} > "$ENV_FILE" )
chmod 0600 "$ENV_FILE"
# Re-source + fall through to the direct install in THIS same invocation.
# shellcheck disable=SC1090
source "$ENV_FILE"
run_direct
;; # run_direct exits
204)
log "not bound yet — will poll again in 30s"
exit 1
;;
410)
log "ERROR: delivery already consumed but no local env — unit will retry (rare crash-window)"
exit 1
;;
404)
log "appliance token not recognized (discarded, or the hub has no record) — will retry in 30s"
exit 1
;;
*)
log "poll returned HTTP ${code:-none} — will retry in 30s"
exit 1
;;
esac
}
# --- mode selection -------------------------------------------------------------------------------
if [[ -n "$FELHOM_CUSTOMER_ID" && -n "$FELHOM_RETRIEVAL_PASSPHRASE" ]]; then
run_direct
else
run_pairing
fi
if [[ ! -s "$SCRIPT_TMP" ]]; then
log "ERROR: fetched host-install is empty — unit will retry"
exit 1
fi
# --- retrieval passphrase -> 0600 tmpfs file ------------------------------------------------------
( umask 077; printf '%s' "$FELHOM_RETRIEVAL_PASSPHRASE" > "$PASS_FILE" )
# --- retry-vs-resume ruling -----------------------------------------------------------------------
args=(--customer-id "$FELHOM_CUSTOMER_ID" --mode "$FELHOM_MODE" --hub-url "$HUB_URL" --passphrase-file "$PASS_FILE")
if [[ -f "$STATE_FILE" ]]; then
log "prior install state present ($STATE_FILE) -> adding --resume (host-install v1.11.3: producers re-run, safe)"
args+=(--resume)
fi
# EXTRA_ARGS are profile-only flags (never secrets); intentional word-split.
read -ra extra <<< "$EXTRA_ARGS"
log "running host-install (customer=${FELHOM_CUSTOMER_ID} mode=${FELHOM_MODE} hub=${HUB_URL})"
bash "$SCRIPT_TMP" "${args[@]}" "${extra[@]}"
rc=$?
cleanup_pass
if [[ $rc -eq 0 ]]; then
log "host-install SUCCESS — writing done-flag, disabling unit, scrubbing env"
install -d -m 0755 "$(dirname "$DONE_FLAG")"
: > "$DONE_FLAG"; chmod 0644 "$DONE_FLAG"
systemctl disable felhom-bootstrap.service 2>/dev/null || true
# Reduce secret-at-rest: the box is enrolled; the passphrase is no longer needed.
shred -u "$ENV_FILE" 2>/dev/null || rm -f "$ENV_FILE"
exit 0
fi
log "host-install FAILED rc=${rc} — unit will retry in 30s"
exit "$rc"
+26
View File
@@ -0,0 +1,26 @@
# Felhom ISO build profile — generic (R-21 slice C, the universal SECRET-FREE ISO).
#
# Build with `--pairing`: the produced ISO carries NO customer-id and NO retrieval passphrase. The box
# installs, registers itself at the hub as an UNCLAIMED APPLIANCE, and the operator binds it to a
# customer on the hub; the hub then delivers the credentials ONCE and day-0 completes.
#
# The hub URL below is baked into the box's pairing env (non-secret). Override per-deployment if the
# box must reach a different hub.
FELHOM_FQDN="felhom-appliance.local"
FELHOM_HUB_URL="https://hub.felhom.eu"
# Disk selection is orthogonal to slice C (credential delivery). This default targets the first SATA
# disk (sda) — correct for single-disk SATA mini-PCs and the nested-VM drill. A box whose target is
# NVMe/other needs a profile variant with a `filter.*` udev match (see README "N100 profile") or an
# explicit disk-list. A match-nothing / wrong disk fails-safe (installer aborts, spike S5c).
FELHOM_DISK_SETUP='[disk-setup]
filesystem = "ext4"
disk-list = ["sda"]'
# Cheap AMI (AN3PLUS-class) boards that can't USB-boot the stock GRUB also need the mkimage loader
# (F1) — uncomment, and set Secure Boot OFF on the target (see profiles/n100.profile):
# FELHOM_LOADER="mkimage"
# Optional emergency/validation key baked into the installed root account (blank -> not baked):
# FELHOM_ROOT_SSH_KEY="ssh-ed25519 AAAA... ops@felhom"
+110
View File
@@ -0,0 +1,110 @@
#!/bin/bash
# bootstrap-modes.sh — R-21 slice C regression harness for felhom-bootstrap.sh's two modes. Runs as
# root inside a throwaway debian container (writes /etc/felhom etc.); fakes curl + host-install +
# systemctl on PATH. Asserts:
# D (regression): a DIRECT env (customer-id + passphrase) enters run_direct and makes ZERO calls to
# /api/v1/appliance/* — the pairing code path is provably not entered.
# pairing: an env WITHOUT customer-id/passphrase enters run_pairing, POSTs /appliance/register,
# persists the token, and GETs /appliance/poll.
# delivery: a poll that returns 200 writes the direct env (0600) and invokes host-install.
set -uo pipefail
BSTRAP=/work/felhom-bootstrap.sh
FAKE=/work/fakebin; rm -rf "$FAKE"; mkdir -p "$FAKE"
CALLS=/work/curl.log
export PATH="$FAKE:$PATH"
fail=0
say() { echo "TEST: $*"; }
check() { if eval "$2"; then echo " ok: $1"; else echo " FAIL: $1"; fail=1; fi; }
# --- fake curl: logs every invocation's URL; emulates -o (fetch), --data (register), -w code (poll) --
cat > "$FAKE/curl" <<'CURL'
#!/bin/bash
url=""; ofile=""; wfmt=""
prev=""
for a in "$@"; do
case "$a" in http*|https*) url="$a";; esac
case "$prev" in -o) ofile="$a";; -w) wfmt="$a";; esac
prev="$a"
done
echo "$url" >> /work/curl.log
mode=$(cat /work/poll-mode 2>/dev/null || echo 204)
case "$url" in
*"/felhom-host-install.sh")
# write a stub host-install to the -o target
cat > "$ofile" <<'HI'
#!/bin/bash
echo "fake host-install ran: $*" >> /work/hostinstall.log
exit 0
HI
exit 0 ;;
*"/appliance/register")
echo '{"appliance_token":"TESTTOKEN123456","poll_interval_sec":30}'
exit 0 ;;
*"/appliance/poll")
if [ "$mode" = "200" ]; then
# body then, if -w set, a newline + code (matches the bootstrap's -w '\n%{http_code}')
printf '%s' '{"customer_id":"drill","retrieval_passphrase":"SEKRET-PASS","mode":"appliance","extra_args":"--cores 2"}'
[ -n "$wfmt" ] && printf '\n200'
else
[ -n "$wfmt" ] && printf '\n204'
fi
exit 0 ;;
esac
exit 0
CURL
chmod +x "$FAKE/curl"
# fake systemctl (disable is a no-op)
printf '#!/bin/bash\nexit 0\n' > "$FAKE/systemctl"; chmod +x "$FAKE/systemctl"
reset_state() {
rm -rf /etc/felhom /run/felhom-bootstrap-pass /var/lib/felhom-install "$CALLS" /work/hostinstall.log /work/poll-mode
mkdir -p /etc/felhom
}
# ============================ Scenario D — direct mode, zero appliance calls =========================
say "D: direct env -> run_direct, NO appliance calls"
reset_state
cat > /etc/felhom/bootstrap.env <<'ENV'
FELHOM_CUSTOMER_ID=acme
FELHOM_MODE=appliance
FELHOM_RETRIEVAL_PASSPHRASE=direct-pass
FELHOM_HUB_URL=https://hub.example
ENV
chmod 0600 /etc/felhom/bootstrap.env
bash "$BSTRAP"; rc=$?
check "run_direct exited 0 (host-install stub succeeded)" "[ $rc -eq 0 ]"
check "host-install was invoked" "[ -f /work/hostinstall.log ]"
check "ZERO /appliance/register calls" "! grep -q '/appliance/register' $CALLS"
check "ZERO /appliance/poll calls" "! grep -q '/appliance/poll' $CALLS"
check "done-flag written" "[ -f /etc/felhom/.bootstrap-done ]"
check "env shredded on success" "[ ! -f /etc/felhom/bootstrap.env ]"
# ============================ pairing mode — registers, then polls ==================================
say "pairing: no customer/passphrase -> register + poll (unbound=204)"
reset_state
cat > /etc/felhom/bootstrap.env <<'ENV'
FELHOM_HUB_URL=https://hub.example
ENV
echo 204 > /work/poll-mode
bash "$BSTRAP"; rc=$?
check "unbound poll -> exit non-zero (systemd retries)" "[ $rc -ne 0 ]"
check "POSTed /appliance/register" "grep -q '/appliance/register' $CALLS"
check "appliance token persisted 0600" "[ -f /etc/felhom/appliance-token ] && [ \"\$(stat -c %a /etc/felhom/appliance-token)\" = 600 ]"
check "GET /appliance/poll" "grep -q '/appliance/poll' $CALLS"
check "no direct env written yet" "! grep -q FELHOM_CUSTOMER_ID /etc/felhom/bootstrap.env"
check "host-install NOT run (unbound)" "[ ! -f /work/hostinstall.log ]"
# ============================ delivery — poll 200 writes env + runs host-install =====================
say "delivery: bound poll (200) -> write direct env + run host-install"
# keep the token from the previous step; flip the poll to 200
echo 200 > /work/poll-mode
bash "$BSTRAP"; rc=$?
check "delivery run exited 0" "[ $rc -eq 0 ]"
check "direct env written with customer-id" "grep -q 'FELHOM_CUSTOMER_ID=drill' /etc/felhom/bootstrap.env || [ -f /etc/felhom/.bootstrap-done ]"
check "host-install invoked after delivery" "[ -f /work/hostinstall.log ]"
check "done-flag written" "[ -f /etc/felhom/.bootstrap-done ]"
echo "=================================================="
if [ $fail -eq 0 ]; then echo "ALL BOOTSTRAP-MODE TESTS PASSED"; else echo "SOME TESTS FAILED"; fi
exit $fail