Live: https://iso.felhom.eu/felhom-installer-1.26.1-pve9.2-1.iso sha256 f3cc86d5f0ec68bba4155c994b4fa84e208d50209bb6e815636c99e5441059a6, 1705322496 bytes. PART 5 PASSED ON BOTH MENU ENTRIES, four observables each: Graphical spikegfx.felhom.eu pairing code J7N-2DA TerminalUI spikesix.felhom.eu pairing code ZY5-YY4 Both: manual install, own disk, own password, real completion signal, and the journal's 'not bound yet — polling every 30s ... normal waiting state, not an error'. Spike 4 had REASONED the graphical path follows from shared Install.pm; it is now measured. PART 6: G1-G10 + G13 all PASS against the uploaded file. G4's single hit is felhom-bootstrap.sh:480's substring TEST ('$envtext' != *FELHOM_RETRIEVAL_PASSPHRASE=*), not a value — my own regex matched the glob's asterisk. PART 7: uploaded via rclone in a container configured ENTIRELY by environment variables, so no credential file was ever written. Round trip verified from the public URL — not the local file. Bucket stays private: unauthenticated GET to the S3 endpoint 400, custom domain has no index (404). CORRECTED BEFORE UPLOAD: the generated manifest described a single automated entry with a 5s timeout and listed Graphical/Terminal UI as 'menu-removed'. Generator fixed, sidecar regenerated, and the ISO verified byte-identical before and after — the published file IS the file Part 5 validated. Hub-side cleared: appliances 16, 17, 18 discarded (303 each); zero rows remain. The endpoint is /appliances/<id>/discard, POST only (server.go:345) — not /delete. Teardown: VMs purged, spike5 storage removed, demo-hp back to 6.6G, drill-r50 and 9201 untouched. Still open and named: OPEN-ITEMS/ROADMAP dispositions for R-128/R-154/R-155 are not written; the .deb is not byte-reproducible (G7 sub-clause); before-network stub unreached; Secure Boot and real hardware not exercised.
29 KiB
Runbook — Day-0 install: first-time deployment of a Felhom host
The complete, step-by-step first-time deployment guide for putting Felhom on a customer's Proxmox box — hub-side onboarding, box prerequisites, the install run, post-install verification, first app deploy, and rollback/uninstall. Every command in this guide was executed verbatim during the clean-room drill (see the header line below); placeholders are marked
<LIKE-THIS>.Drill-validated: 2026-07-03 on a clean-room nested PVE 9.2.2 VM (qemu/KVM on the build server) — install → verify → app deploy → uninstall → residue-diff → reinstall, all executed verbatim. Versions: script v1.9.1, agent v0.63.0, golden v0.98.3 (controller current on first boot — no manual update step). Evidence:
documentation/audits/DRILL-day0-cleanroom-2026-07-03.md(install/uninstall path) +documentation/audits/DRILL-golden-098-2026-07-03.md(current-golden rebuild).Scope: a shared Proxmox box (the colleague's-box model) — a box that already runs, or will run, non-Felhom guests. A dedicated box is the same procedure minus the sizing caution.
v1.10.0 (GL-2): every install now REQUIRES
--mode appliance|byo— there is no default. Useappliancefor a box Felhom owns/manages end-to-end (this guide's drilled path, unchanged apart from the flag); usebyofor a host the operator does NOT own (the pilot's own PVE) — see §C.5. The byo profile is implemented + statically validated but not yet drill-validated (that is the supervised GL-6 run).
Who does what
| Actor | Where | Does |
|---|---|---|
| Operator | HQ (hub UI + Cloudflare) | Part A (customer onboarding), go/no-go, on call during install |
| Operator (or trusted colleague with root SSH) | the customer's Proxmox box | Parts B–E |
| Customer | — | confirms the Part B prerequisites, receives the dashboard URL |
One secret crosses to the box: the customer's retrieval passphrase (5 Hungarian words, generated by the hub in Part A). Everything else the box needs, it fetches and verifies itself.
Part A — hub-side customer onboarding (operator, at HQ)
A.1 Create the Cloudflare tunnel (per customer domain)
The customer's apps are published through a Cloudflare Tunnel; the in-guest controller runs
cloudflared with a per-customer token. In the Cloudflare dashboard (Zero Trust → Networks →
Tunnels):
- Create a tunnel named after the customer (e.g.
<customer-id>). Connector type: Cloudflared. - Copy the tunnel token (the long base64 string from the
cloudflared service install <token>command) — this goes into the hub customer form in A.2. - In the customer domain's DNS zone, the controller manages per-app hostnames itself via the tunnel; make sure the domain is on Cloudflare (nameservers active).
Also have ready (optional but recommended): a Cloudflare API token with Zone edit rights for the customer's zone — the hub uses it for geo-restriction management.
A.2 Create the customer in the hub
Hub UI (https://hub.felhom.eu, operator password) → Customers → New:
| Field | Value | Notes |
|---|---|---|
| Customer ID | e.g. sajatfelhom |
letters, numbers, dots, hyphens only; immutable — it keys everything |
| Customer name | display name | |
| Domain | e.g. sajatfelhom.hu |
the customer's app domain (on Cloudflare) |
| customer's email | used for customer-tier notifications (Hungarian) | |
| CF tunnel token | from A.1 | → infrastructure.cf_tunnel_token |
| CF API token | from A.1 (optional) | → infrastructure.cf_api_token (geo rules) |
| Git username | Gitea read account | → git.username — optional (only for a private app catalog) |
| Git token | Gitea read token | → git.token — optional since installer v1.11.2 (the G3 ruling): artifacts are world-readable and fetched ANONYMOUSLY, sha256-verified against the hub manifest. Empty is the normal customer shape; expect the installer's "fetching artifacts ANONYMOUSLY" warn line in steps 5/8 (drill-verified 2026-07-12 on the VM Day-0 drill). |
On save the hub generates two credentials:
- Retrieval passphrase (5 Hungarian words) — the ONE secret the installer carries to the box. Dictate or hand it to whoever runs Part C. Treat it like a password.
- Customer API key — internal (baked into the generated controller.yaml); never handled manually.
A.3 Verify the Day-0 artifact manifest
Hub UI → Configuration → Day-0 artifacts. The manifest must vouch an agent version and a golden version (with their sha256s — auto-resolved from Gitea). The install script refuses to install anything the manifest doesn't vouch. If the vouched versions lag the fleet, bump them here first (dropdowns; the sha is fetched authoritatively). Current drilled-known-good: agent 0.63.0 + golden 0.98.3. Vouch a golden ≥ 0.98.3 — an older golden bakes a pre-floor controller and re-introduces the manual update step (Part F troubleshooting).
A.4 Check the global controller-version floor
Hub UI → Configuration. Know the current min_controller_version floor. With golden ≥ 0.98.3
vouched (the structural fix for the old OQ-2 finding — drill-proven 2026-07-03), a fresh box lands
on a current, floor-honoring controller and self-manages from first boot; the floor's job is
keeping it current afterwards. Recommended (1 minute, records drift-protection): raise the global
floor to the current controller version whenever the golden is rebuilt.
Part B — box prerequisites (confirm BEFORE install day)
Collect these from the customer / the box ahead of time. Every line is checkable in two minutes; a "no" on any of them turns install day into debugging day.
[ ] Proxmox VE 9.x installed and reachable over SSH as root
pveversion # want pve-manager/9.x
[ ] Single node or cluster?
pvesh get /nodes --output-format json | python3 -c 'import json,sys; ns=json.load(sys.stdin); print(len(ns), [n["node"] for n in ns])'
→ more than one node: the install command MUST carry --node <name>
[ ] local-lvm free space ≥ 120 GiB
lvs /dev/pve/data -o lv_size,data_percent
[ ] Free vmid chosen from the FULL guest list (never from the hub view)
pct list; qm list # pick a vmid not in either list; avoid 9999 and 990000-990009
[ ] Host cores + RAM known (to size the appliance cap)
nproc; free -m
→ shared box: cap the guest so the other guests keep breathing (Part C uses --cores/--memory)
[ ] Outbound HTTPS reachability from the box:
curl -fsSo /dev/null -w '%{http_code}\n' https://hub.felhom.eu # want 302 (login redirect)
curl -fsSo /dev/null -w '%{http_code}\n' https://gitea.dooplex.hu # want 200
curl -fsSo /dev/null -w '%{http_code}\n' https://felhom.eu # want 200
[ ] python3 present (PVE 9 default: yes)
command -v python3
[ ] The operator has the retrieval passphrase from Part A at hand
Notes:
- The box needs no inbound reachability — hub, Gitea and the Cloudflare tunnel are all outbound-only.
- vmid picking: the agent's token is pool-scoped, so the hub's guest inventory only shows
Felhom-pool guests — other guests on a shared box are invisible there. Always pick from
pct list+qm liston the box (documentation/runbooks/provisioning.md). - DR-tier-by-default (v1.15.0, 2026-07-12): every install deploys the FULL DR plumbing
uniformly — the
felhom-pbs-applywrapper,age,wg_tunnel.enabled: true(WG is base infrastructure like cloudflared), and the default ACL incl. the pre-positioned/storage/felhom-pbsgrant. Capability is baked; ACTIVATION is the hub's per-customer "DR tier" flag (default ON for new customers). With the flag ON the cascade converges hands-free after install: WG peer registers → the hub auto-provisions the ep0 tenancy → the agent applies the storage entry → the escrow ceremony becomes possible. Flag OFF = zero Felhom-side cost; the pbsdr capabilities report a neutral "inactive". Offsite app backup REQUIRES the tier (the ceremony depends on the PBS key — drill F-6, closed by policy). Design-of-record:documentation/architecture/06-offsite-connectivity.md.
C.0 Two ways in — which to use
The installer ISO (https://iso.felhom.eu/) is the normal path for a NEW machine. Write it to a
USB stick, boot it, choose the disk and a root password in the standard Proxmox installer, and the box
sets itself up on first boot and shows a pairing code on the console. Nothing is baked into the image:
no root password, no SSH key, no customer identity, no disk selection. The image offers two entries —
graphical (default) and Terminal UI — and both were verified end-to-end before publication
(package installed, unit enabled, unit fired on first boot, pairing code displayed). See
REPORT-iso-release.md and the criteria in iso-release-gate.md.
The manual one-liner below is for a machine that ALREADY runs Proxmox — a box someone else
installed, a re-pair after a reset, or an install where the ISO path did not complete. It is also the
recovery path: it does what the ISO's package would have done, and the hub's customer page renders it
with the customer-id already filled in (hub/internal/web/templates/customer_unified.html:563).
Neither supersedes the other. The ISO is insert, install, type a claim code. The one-liner is already installed — adopt this box.
Part C — the install run (on the box, as root)
C.1 Fetch the installer
curl -fsSO https://felhom.eu/scripts/felhom-host-install.sh
chmod +x felhom-host-install.sh
./felhom-host-install.sh -h | head -3 # sanity: must print v1.15.0 (or newer) — DR-tier-by-default ships the full plumbing
This URL is the website's git-sync working tree tracking main on a 30-second period
(manifests/webpage.yaml) — it is always the current main script. There is no release tag, no
staging copy and no version selector; pushing scripts/felhom-host-install.sh publishes it.
C.2 Preview (recommended)
--dry-run prints every mutating command without executing anything (it still contacts the hub
read-only and asks for the passphrase):
./felhom-host-install.sh --customer-id <CUSTOMER-ID> --mode appliance --vmid <VMID> \
--cores 2 --memory 4096 \
--force-gitea-golden \
--dry-run
C.3 The canonical shared-box install command
./felhom-host-install.sh --customer-id <CUSTOMER-ID> --mode appliance --vmid <VMID> \
--cores 2 --memory 4096 \
--force-gitea-golden
Placeholders and flags — what and why:
| Flag | Value | Why |
|---|---|---|
--customer-id |
the Part A customer ID | keys every hub call |
--mode |
appliance or byo |
REQUIRED, no default. appliance = a Felhom-owned/managed box (this section). byo = a host the operator does not own — use §C.5's command instead |
--vmid |
the free vmid from Part B | ALWAYS pass it explicitly — deterministic, and recorded in the install state for a later --uninstall |
--cores 2 --memory 4096 |
the appliance cap | protects the other guests on a shared box; size to roughly half the host if the box is small (cap ≤ host resources) |
--force-gitea-golden |
— | a fresh box has no local golden archive; fetch it from Gitea and verify its sha256 against the hub manifest (this is the normal customer path) |
--acl-storages |
do NOT pass it | the DEFAULT set (local local-lvm felhom-pbs) is correct — the felhom-pbs grant is PRE-POSITIONED for the PBS-DR tier (the storage appears when the tier applies). Narrowing it 403'd the apply-bridge live (DRILL-day0-vm-2026-07-12: Datastore.Allocate on /storage/felhom-pbs) and needed a retrofit dual-grant. Only override on a box with genuinely different storage names. |
--node <NAME> |
only on a cluster | the script refuses to auto-pick a node on a multi-node cluster |
When prompted, type the retrieval passphrase (no echo; never on the command line). Alternative
for remote/scripted runs: put it in a 0600 file and pass --passphrase-file <path>.
C.4 What the script does (so you can follow the output)
Eight steps, all idempotent/resumable (--resume skips completed steps after a mid-run failure):
- pre-flight — root/PVE/hub checks, cluster refusal without
--node, LVM/RAM floors, passphrase validated read-only against the hub, vmid guard. - Proxmox API token —
felhom-agent@pve!agentminted (privsep), thefelhompool created, and the pool-scoped 3-role ACL applied (blast-radius containment: the token can only touch Felhom-pool guests + the granted storages). - volume grows — computed from free space (Docker-data/user-data sizing).
- host enrollment —
POST /host-enroll(passphrase-authed, idempotent) mints/returns the per-host hub credential. - agent install — binary fetched from Gitea, sha256-verified against the hub manifest, installed with the non-root service user, sudoers (visudo-validated), guarded-mkfs wrapper, systemd unit.
- agent config + service — config written 0600, read-only selftest must pass, daemon started.
- golden archive — fetched from Gitea + sha256-verified (because
--force-gitea-golden). - provision — the agent restores the golden into vmid
<VMID>(into thefelhompool), grows volumes, rendersbootstrap.json, sets onboot, starts the guest. Then a verify pass.
Expected duration: minutes; dominated by the two Gitea downloads (agent ~20 MB, golden ~1-2 GB) on the customer's uplink.
If a step fails: read the error (they are specific), fix the cause, re-run the SAME command with
--resume appended (with the SAME --mode — the script refuses a mode flip on a half-done install).
C.5 BYO hosts (--mode byo) — installing on a Proxmox server the operator does NOT own
Trust model. On a BYO host the roles invert: the box owner is root and stays root; Felhom is the
guest. The installer therefore (a) never touches the owner's credentials — the break-glass step
(4b/8, root@pam reset + hub vault) is skipped entirely, nothing is vaulted; (b) treats the owner's
workloads as the thing to protect — the CPU/RAM caps are mandatory, they are the only
noisy-neighbor containment there; (c) refuses anything host-invasive that the appliance profile
allows (--enable-oob, --rotate-recovery, unvalidated PVE majors, ACL grants on storages the box
doesn't have); and (d) asserts the written agent config keeps lan_resolver/oob OFF
— the agent must not take over the owner's DNS or start a second sshd. (wg_tunnel is ON in
BOTH modes since v1.15.0 — WG is base infrastructure; byo consent = the typed disclosure ack,
which now lists the outbound tunnel, + the tester-agreement disclosure line.) Recovery on a BYO box = the owner's
own console access, by design.
Recommended first run — preflight only (all step-1 checks incl. the byo gates, an explicit PASS/FAIL verdict, exit 0/1, writes nothing — re-runnable any number of times):
./felhom-host-install.sh --customer-id <CUSTOMER-ID> --mode byo --vmid <VMID> \
--cores <N> --memory <MiB> \
--force-gitea-golden \
--preflight-only
The install command (same flags, minus --preflight-only):
./felhom-host-install.sh --customer-id <CUSTOMER-ID> --mode byo --vmid <VMID> \
--cores <N> --memory <MiB> \
--force-gitea-golden
What byo does differently (everything else matches C.4's eight steps):
--coresAND--memoryare required — size them WITH the owner (the guest must never starve the owner's guests).--acl-storages(when overridden) must name storages that actually exist on the box, or the preflight dies naming the missing ones — EXCEPTfelhom-pbs, which is expected absent pre-tier (the PBS-DR apply creates it; the grant is pre-positioned).- The host's
:53must be free (v1.12.0/F6): Felhom publishes the guest by name on the LAN, so a resolver already bound to:53blocks it. byo preflight REFUSES (does not touch the owner's service — it is a refusal, not a change) with: "a resolver is already bound to :53 … stop or reconfigure it, or point your LAN DNS at the guest's address, then re-run." Clear:53(or point the owner's DNS at the guest) and re-run. (Appliance mode is different — there Felhom's own lan_resolver owns:53.) - A fresh install on a populated BYO host Just Works (v1.12.0/F3): the leaf-regeneration guard
now fires only when a Felhom guest is already present — the owner's own non-Felhom guests no
longer trip it, so
--allow-new-leafis NOT needed on a first install. (You still need--preserve-state-from/--allow-new-leafon a reinstall over an existing Felhom guest, to protect its pinned fingerprint — unchanged.) - Preflight also prints the existing-guest count.
- At the end of preflight the script prints the complete host-mutation list (pveum roles/user/token/ACL/pool, agent user/binary/unit/sudoers/config/state, the watchdog + guarded wrappers, the guest + golden) and requires typing the host's short hostname to proceed — show this list to the box owner; it is the consent artifact.
- Step 4b/8 (break-glass) is skipped; the verify step asserts pool membership + the scoped ACL grants landed (both modes do this from v1.10.0).
C.5b Operator key pin — self-update authority (v1.11.0)
The agent's self-update only acts on operator-signed update ops; the verifying public keys are
pinned in the agent config (authz.signers). A fresh install with no key pinned runs self-update
DORMANT (the safe default — the verify step prints a WARN, not an error). To arm it at install
time, either:
- fill the
OPERATOR_KEY_*constants near the top of the script (one commit, after the offline key-generation ceremony), or - pass
--operator-pubkey-file <path>— one key per line,operational <authorized_keys line>orrecovery <authorized_keys line>; the authorized_keys comment field is the key_id and is required. The file overrides the constants.
Rules: PUBLIC keys only (private keys never touch the box or this script); a reinstall with no
keys resolved keeps an already-pinned config's signers (--preserve-from) — pinned boxes are
never silently un-pinned; providing keys over a preserved config replaces them (pin rotation,
logged). byo installs show the operational key_id in the disclosure block.
C.6 Post-hoc mode note for pre-v1.10.0 installs
Boxes installed by ≤ v1.9.1 have no recorded mode; their state file simply predates it. --resume
on such a box accepts whichever --mode you pass — pass the mode that matches how the box is
actually operated (existing Felhom-owned boxes: appliance).
Part D — post-install verification (on the box + hub)
Run through ALL of these before calling it done.
D.1 On the box
# 1. The script's own final verdict said: "Day-0 provision SUCCESS — vmid=… host_id=…"
# 2. Agent service up, running non-root
systemctl is-active felhom-agent # active
systemctl show felhom-agent -p User --value # felhom-agent
# 3. Agent selftest — all capabilities, including the pool read (stale-lock reaper)
felhom-agent --config /etc/felhom-agent/agent.json --selftest 2>&1 | tail -25
# want: no FAILs; the "pve:pool-read" line ok (agent >= v0.62.0)
# 4. Guest up + onboot
pct status <VMID> # status: running
pct config <VMID> | grep -E '^(onboot|rootfs|mp0|mp1)'
# 5. Controller container healthy in-guest
pct exec <VMID> -- docker ps --filter name=felhom-controller --format '{{.Status}} {{.Image}}'
# 6. Dashboard answers in-guest (the controller sits BEHIND traefik — probe with the Host header;
# the controller's own :8080 is internal to the Docker network and not directly reachable)
pct exec <VMID> -- curl -ksSo /dev/null -w '%{http_code}\n' \
-H "Host: felhom.<CUSTOMER-DOMAIN>" https://127.0.0.1/ # want 200
D.1b Controller version check (one line — the old manual update step is RETIRED)
The golden (≥ 0.98.3) bakes a current, floor-honoring controller — a fresh install is current on first boot and the hub floor manages it from here. Just verify:
# controller version current on first boot; the floor manages it from here (want: update_available false)
pct exec <VMID> -- curl -ksS -X POST -H "Host: felhom.<CUSTOMER-DOMAIN>" https://127.0.0.1/api/selfupdate/check
If this reports the baked version as BEHIND (a newer controller shipped after the golden was baked),
the same endpoint's /api/selfupdate/update — or the dashboard's Beállítások → "Frissítés" button —
converges it; that is the normal self-update path, not an install-day requirement. (Installed from a
golden older than 0.86.0? See Part F troubleshooting.)
D.2 In the hub UI
- Hosts: the new host reports (status OK, fresh last-report timestamp).
- The host's guest list shows
<VMID>running. - Capabilities (rendered on the host page since hub v0.51.0): 0 degraded — v1.15.0 ships
the full DR plumbing, so the drill's "3 pbsdr-* born DEGRADED" is gone. With the customer's DR
tier flag OFF the pbsdr capabilities show a neutral inactive ("disabled by configuration")
chip — deliberate, not a fault. A pbsdr
degraded — binary not foundmeans the box predates v1.15.0; the host page shows the retrofit one-liner (wrapper +age+wg_tunnel.enabled). - The controller version shown for the guest is the golden's baked (current) version — it matches what D.1b's check reported.
D.3 Customer-visible
https://felhom.<customer-domain>answers through the tunnel (cloudflared up in-guest:pct exec <VMID> -- docker ps --filter name=cloudflared).- Customer-claim gate (controller ≥ v0.122.0): a fresh box is CLAIM-GATED, not open — the URL
serves the claim page ("A szerver beállítása") and every other route returns the claim page (302)
or 401. This is correct and expected (closes the DRILL-day0-vm F-4 exposure). The hub emails a
one-time claim code to the customer's registered address on the box's first report; the customer
claims by entering the code + choosing their own password. Verify the gate mechanically without
claiming:
curl --resolve felhom.<domain>:443:<CF-edge-IP>on/→ 302, on/claim→ 200, on/api/host-metrics→ 401. Do NOT claim on the operator's behalf — that is the customer's step (onboarding). (An older <0.122 controller has no gate and is open until claimed post-update — the transitional legacy-open red banner shows until the hub delivers a code hash.)
D.4 First app deploy (smoke)
Deploy one small dependency-free catalog app end-to-end — after the customer has claimed the dashboard (the gate blocks deploys pre-claim). The app reaches "running" and its URL answers, proving the full chain: catalog → controller → Docker → tunnel → browser. (For an operator-only smoke before claim, drive the in-guest Host-header probe instead — the public path is gated.)
Part E — rollback / uninstall
--uninstall cleanly reverts an install — no hub contact, no passphrase needed:
./felhom-host-install.sh --uninstall --vmid <VMID>
Guardrails (all verified in the drill):
- Ours-check: refuses to destroy a guest without the
/etc/felhom-bootstrapmount (i.e. not Felhom-provisioned) unless--force. - Typed confirmation: you must type the vmid back before anything is destroyed.
- Shared-box safety: if OTHER Felhom guests remain on the host, only the named guest is
removed; the agent/token/ACL stay (guest-only mode).
--forceoverrides. - The
felhompool is deleted only if empty. --vmidis optional when the install state file recorded the provisioned vmid (a fresh v1.9.x install records it); passing it explicitly is still the recommended, deterministic form.
What it removes (v1.9.1, residue-diff-proven — a full-filesystem diff against the pre-install
baseline showed zero Felhom-named leftovers): the guest, the agent (unit/sudoers/binary/
state dir/config/service user), the shared-parent unit + wrapper + /mnt/felhom-drives, the
guarded-mkfs wrapper, the guest-hook snippet, the lan-resolver dnsmasq snippets, the pveum
roles/ACL/token/user, the pool (if empty), the install state file.
v1.11.0 additions: the teardown also removes the self-update artifacts (guarded wrapper, A/B
slots, rollback unit, limits drop-in), unmounts enrolled drives under /mnt/felhom-drives/
(plain umount only — a busy drive gets a warning and guidance, never a forced unmount; the data is
never touched and the drives are physically removable afterwards), and both modes end with an
explicit KEPT-vs-WIPED statement — read it before pulling drives or closing the customer out
(it lists what lives on: drive data, PBS backups, the hub record, the escrow blob, the vaulted
recovery credential).
Expected remnants (documented, not residue):
- The hub host record (+ its report/guest history) — the hub currently has no host-delete; removing it is a direct-DB operator action (recorded follow-up).
- The
sudoanddnsmasqpackages (system packages installed as dependencies; inert). - The golden vzdump on the archive storage — pass
--remove-goldento delete it too. - System noise: LVM archive metadata, dpkg lists, PVE's own rrd/
/var/lib/lxcscratch entries.
Part F — troubleshooting (from the drill + live history)
| Symptom | Cause | Fix |
|---|---|---|
| dies immediately: "--mode is required" | v1.10.0+ has no default profile | add --mode appliance (Felhom-owned box) or --mode byo (owner's box, §C.5) |
| dies immediately: "byo mode requires explicit --cores and --memory" | byo caps are mandatory | size the caps with the box owner and pass both |
| dies: "install started as X; resume with --mode X" | --resume with the other mode |
resume with the recorded mode, or --uninstall and start over |
| byo dies: "acl storage(s) not found on this box" | --acl-storages names a storage the box lacks |
pass the box's real storages (check pvesm status) |
| step 1 dies: "this is a N-node cluster" | multi-node cluster | re-run with --node <name> |
| step 5 dies: "hub artifact manifest has no agent version" | Day-0 manifest unset/incomplete | Part A.3 — set it in the operator UI |
| step 5 warns: "no git credential — fetching artifacts ANONYMOUSLY" | customer has no git credentials — the NORMAL shape since installer v1.11.2 | nothing to do; sha256 verification is unchanged. (Pre-v1.11.2 installers die here instead — upgrade the script.) |
| step 1: passphrase REJECTED (401) | typo / wrong customer | re-check with the hub UI's printed curl command |
| step 8 fails: "CT already exists" | vmid collision with a hub-invisible guest | pick from pct list + qm list (Part B); the agent destroys nothing on collision — re-run with a free vmid and --resume |
| controller container missing in-guest after provision (docker ps empty) | golden older than 0.98.3 (no baked path unit) + pre-v1.9.1 script: the controller-bootstrap unit's boot-time condition lost the race with the bootstrap-mount attach | pct reboot <VMID> — the unit runs on the next boot (v1.9.1 reboots itself; goldens ≥ 0.98.3 bake a path unit that starts the controller on the mount hot-plug, no reboot needed) |
| fresh box runs an OLD controller and never self-updates (hub floor ignored) | a golden older than 0.86.0 was vouched in the Day-0 manifest — its baked controller predates the floor-honoring code | one-time manual update (the retired old D.1b): dashboard Beállítások → "Frissítés", or POST /api/selfupdate/check + /api/selfupdate/update via the traefik Host-header probe; if the agent is < v0.63.0, systemctl restart felhom-agent FIRST (pre-v0.63.0 agents 401 the swap call). Structural fix: re-vouch a golden ≥ 0.98.3 (A.3) |
| self-update fails: "Agent swap request failed: HTTP 401" (or dashboard agent calls 401) | agent < v0.63.0: the daemon's token index missed a token minted by the provisioner after it started (FIXED in agent v0.63.0 — reload-on-miss) | systemctl restart felhom-agent, then retry (unnecessary from agent v0.63.0) |
local-lvm free < 120 GiB warning |
small/shared box | proceed only if you sized the grows deliberately; otherwise free space first |
| cloudflared restarting in-guest | bad/missing tunnel token | re-check A.1/A.2 token, regenerate config, docker restart cloudflared in-guest |
(Extended during the drill with every real hiccup encountered.)
Related
- vmid picking + reserved bands:
documentation/runbooks/provisioning.md - The Day-0 trust chain (why the box can verify what it fetches):
documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md - Pool-scoped ACL model:
documentation/audits/SPIKE-pool-scoped-acl-2026-07-01.md - Secrets handling:
documentation/runbooks/secrets.md