Tester gate: golden re-baked to 0.188.0, fresh-install proof PASSED — a fresh box is safe to hand to a tester

§7.2 answer: YES. A real day-0 from the existing v1.25.0 ISO reached a claimable,
app-serving box in ~10 minutes unattended, and an app's data came back from the
drive with the guest's app.yaml gone — proven readable by the application over
its own TCP path, with a discriminator (PRE-BACKUP row = 1, POST-BACKUP row = 0).

Part 0: NO ISO rebuild needed, verified against the ISO on disk rather than from
source. It bakes only felhom-bootstrap.sh, its unit and the secret-free pairing
env (full-base64 match, 1 hit each) and 0 hits for any installer, controller or
golden marker. The installer is fetched at run time; the live URL is byte-identical
to repo HEAD (v1.22.0, six days newer than the ISO) and the fresh box ran it.

Part 1: baked 0.188.0 rather than the brief's 0.187.0 — 0.187.0 lacks D5, which
is the very claim Part 2 step 6 tests. Published (404 pre-gate with a 200 control;
anonymous download, 649310288 bytes, sha match), vouched, and consumed by a real
box. R-120's gate exercised BOTH ways: 0.185.1 refused with no write, 0.188.0
allowed — evaluated, not silently skipped.

Part 3: RUNBOOK-manual-build.md cited a "RECORDED" qemu line that is itself
labelled reconstructed and whose source says it was never saved. The real
invocation is now captured from this bake as §4.0, with the bake/publish/teardown
steps; the old entry is marked SUPERSEDED.

Teardown all three layers, hub disposition stated: VM destroyed, scratch storage
removed with space returned exactly, customer sess-g DELETED via full cascade.
sess-f deliberately left (R-131) with its command recorded.

Filed, none fixed: R-128 (false ISO_VERSION invariant comment), R-129 (demo-hp's
"no baked SSH key" is stale — key auth works), R-130 (HARD_MIN_LVM_GIB warns and
proceeds), R-131 (fourth orphaned scratch customer), R-132 (curl's %{redirect_url}
printed the hub operator password into a transcript — HUB_PW needs rotating).
This commit is contained in:
2026-07-31 08:27:36 +02:00
parent 37b93dc583
commit b4edc087fa
5 changed files with 568 additions and 10 deletions
+98
View File
@@ -0,0 +1,98 @@
# REPORT — tester gate: golden re-baked to 0.188.0, fresh-install proof PASSED (2026-07-31)
Written as `REPORT-<topic>.md` per `CLAUDE.md:82-87` so the shared `REPORT.md` (E-2 increment 1) is
not clobbered. Full record with every observable: `documentation/audits/tester-gate-golden-0.188.0-2026-07-31.md`.
## Outcome
**§7.2 — YES: a fresh install is safe to hand to an external tester.** ISO boot → claimable,
app-serving box in ~10 minutes unattended, and an app's data restored **from the drive with the
guest's `app.yaml` gone**, proven readable by the application over its own TCP path.
**Golden 0.186.0 → 0.188.0** baked, published, vouched. **No ISO rebuilt** — Part 0 proved none was
needed. No existing box changed; floor still v0.156.0, MinAgent still 0.113.0.
## Part 0 — the ISO does not need rebuilding
Verified against the ISO **on disk**, not from source. It bakes exactly three Felhom payloads
(`felhom-bootstrap.sh`, its unit, the secret-free pairing env) — full-base64 match, 1 hit each — and
**0** hits for `SCRIPT_VERSION="1.2`, `felhom-controller`, `vzdump-lxc-9100`. The installer is fetched
at run time (`felhom-bootstrap.sh:96`) and the live URL is byte-identical to repo HEAD
(sha `ab8b283e…`, v1.22.0, committed six days *after* the ISO). The golden arrives via the hub-vouched
artifact manifest (`felhom-host-install.sh:423-433`). The one genuinely baked, drift-capable thing is
`felhom-bootstrap.sh` itself — currently at repo HEAD.
**Proven live**, not just argued: the fresh box ran `felhom-host-install v1.22.0` and fetched golden
**v0.188.0**, sha-verified.
## Part 1 — bake / publish / vouch
Baked **0.188.0**, not the brief's 0.187.0: 0.187.0 lacks D5, and Part 2 step 6 *is* the D5 claim, so
that golden could not have passed the proof this task exists for. 0.188.0 satisfies R-120 anyway.
Stated rather than absorbed, per standing rule 4.
GOLDEN_VERSION=0.188.0
GOLDEN_SHA256=7353d8beb63641f87a848e45f8aa12e465647e1190ad164a65b32ad01fc3d299
Three observables: 404 pre-gate (with a 200 control on 0.186.0 so it is not vacuous), then an
**anonymous** download returning `http=200 bytes=649310288` and a matching sha; the manifest read back
showing `0.188.0` selected; and the consumer call `GET https://hub.felhom.eu/api/v1/artifacts/sess-g`
returning the pair. Plus a fourth: a real fresh box fetched and sha-verified it.
**R-120's gate evaluated and allowed.** Exercised both ways rather than inferred from silence —
vouching 0.185.1 first produced `flash=golden_behind_fleet`, the logged `artifact vouch REFUSED`, and
**no write** (the manifest still read 0.186.0); then 0.188.0 produced
`Artifact manifest set: agent=0.118.1 golden=0.188.0`.
## Part 2 — the clean-install proof, on demo-hp
All seven steps PASS. Venue was demo-hp (Tier 0, the designated drill host) using the scratch dir
storage at `/mnt/nvme-1tb` that `target-selection.md:38-40` names; `local-lvm`, `drill-r50` and both
9201s untouched.
Highlights: real day-0 pairing → bind → install; a **real** claim (the code is emailed-only, R-119 —
the operator relayed it), with the gate flipping `dashboard not yet claimed``authentication
required`; controller **0.188.0** confirmed *from the box*; **rallly** (postgres) + **homebox**
deployed through the real endpoints.
The D5 leg: recovery unit `portable-carried=2/2, withheld=0`; the carried `DB_PASSWORD` matched the
live one **by fingerprint** (`14c8f515…`, never printed); guest `app.yaml` moved aside; restore
returned `secrets recovered=2/2`. Step 7 read the data from **rallly's own network namespace** over
TCP to `rallly-postgres` — not the localhost trust socket that produced D5's false pass — and the same
path with a wrong password returned `FATAL: password authentication failed`, proving the credential
does real work. **The discriminator held: PRE-BACKUP row = 1, POST-BACKUP row = 0.**
## Part 3 — runbook integrity
`RUNBOOK-manual-build.md` told the reader to use a "RECORDED" qemu line that is itself labelled
*reconstructed*, and whose source says it *"was never saved"*. The real invocation is now captured
from this bake and recorded as canonical in **§4.0**, alongside the bake/publish/teardown steps, the
template-rot warning and where the R-120 gate actually lives. The old runbook's deviation entry is
marked SUPERSEDED with a forward pointer.
## Teardown — three layers
1. VM 310 destroyed with `--purge --destroy-unreferenced-disks 1`; `/mnt/nvme-1tb/images/` empty.
2. `cc-scratch` removed, `storage.cfg` back to its original four entries; `felhom-backup` available
**926 492 284 KiB before and after** — space returned exactly.
3. Hub: **`sess-g` and its host record DELETED, full cascade** — `customer DELETE cascade COMPLETE
for sess-g (journal #8) — full teardown`, residue purged including `appliance_registrations=1`.
Verified positively: `/configs` and `/hosts` both loaded (10160 / 9880 bytes) with **0** hits for
`sess-g` and 0 for the appliance UUID. The gate refused twice first (409 host ONLINE, then 400
missing acknowledgements) — the record cannot be deleted until the destroyed box ages out of
ONLINE, ~30 min. **`sess-f` deliberately NOT deleted** (R-131); its command is in the audit §7.1.
Secrets shredded in the guest and on the box.
## Findings — filed, none fixed
`R-128` ISO_VERSION/SCRIPT_VERSION comment is false · `R-129` demo-hp's "no baked SSH key" is stale
(key auth works) · `R-130` `HARD_MIN_LVM_GIB` warns and proceeds — a hard min that is not hard ·
`R-131` `sess-f` is a fourth orphaned scratch customer · **`R-132` — `curl -w '%{redirect_url}'`
printed the hub operator password into a session transcript; `HUB_PW` needs rotating.**
## Not done, deliberately
No ISO built; no defect fixed; no golden deployed to an existing box; no floor or MinAgent change;
offsite/PBS-DR legs not exercised (the task forbids pointing anything at production PBS or the real
restic offsite, so `sess-g` ran DR-tier off); Campaign 10, the demo-hp repartition and subdomain
onboarding untouched.
@@ -0,0 +1,402 @@
# Tester gate — re-bake the golden, and prove a fresh box is safe to hand to a tester (2026-07-31)
**The sentence that matters (§7.2): YES** — a fresh install from the existing v1.25.0 ISO is safe to
hand to an external tester. Installed clean and unattended, registered, claimed, deployed two apps,
and restored an app's data with the guest's `app.yaml` gone — with the data proven readable by the
application over the path the application actually uses. Caveats in §8; none of them blocks a tester.
Golden re-baked **0.186.0 → 0.188.0** (D5), published, and vouched. No ISO was rebuilt — Part 0 showed
none was needed. Nothing already installed changed.
---
## 1. Baselines — established separately, at the start of this run
| What | Value | How |
|---|---|---|
| controller `main` | **v0.188.0** (D5) | `HEAD`=`origin/main`=`4115e88`, tree clean |
| controller on `demo-felhom` (9201) | 0.187.0 | `pct exec 9201 -- cat /etc/felhom-controller-image` |
| controller on `demo-hp` | 0.187.0 | hub `/configs` row (the box's own report) |
| **golden's baked controller, before this run** | **0.186.0** (sha `b760ac6a…`) | hub manifest; `drill/bake-0.186.0.log` |
| agent published + vouched | 0.118.1 (sha `9a74c87d…`) | hub manifest; matches `felhom-agent --version` on felhom-pve |
| hub | 0.83.0 | `deploy/hub` image tag |
| ISO | v1.25.0, built 2026-07-23 | `build-felhom-iso.sh:44`; on-disk manifest |
| `felhom.eu` HEAD | `0a9bd38`, clean | `git rev-parse` |
| managed floor | v0.156.0 (DB override) | hub `/configuration` |
The brief expected golden 0.186.0 and fleet 0.187.0 — both correct. It also assumed `main` was
0.187.0; it is **0.188.0**. See §3.
## 2. Part 0 — does the ISO need rebuilding? **NO**
**1. What the ISO contains.** Three Felhom payloads, base64-injected into the first-boot stub
(`scripts/iso/stub-first-boot.sh:22-38`, filled at `scripts/iso/build-felhom-iso.sh:324-329`):
`felhom-bootstrap.sh`, `felhom-bootstrap.service`, and the bootstrap env. Plus `answer.toml` (fqdn,
disk-setup, a per-build throwaway root hash) and the GRUB branding. **No controller image, no golden
archive, no installer copy.**
Verified against the ISO **on disk**, not inferred from source — full-base64 match, one hit each:
| Probe | Hits |
|---|---|
| base64 of repo-HEAD `felhom-bootstrap.sh` (36 692 chars) | **1** |
| base64 of repo-HEAD `felhom-bootstrap.service` | **1** |
| base64 of the reconstructed secret-free pairing env | **1** |
| `SCRIPT_VERSION="1.2` (host-install marker) | **0** |
| `felhom-controller` | **0** |
| `vzdump-lxc-9100` (golden marker) | **0** |
**2. What the bootstrap fetches at run time.** `felhom-bootstrap.sh:96`
`INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}"`, fetched on
every attempt at `:322-326`. Served from the repo's `/scripts` by git-sync
(`manifests/webpage.yaml:71-75`). Live check: 200, and **byte-identical to repo HEAD** (both sha256
`ab8b283e…`), SCRIPT_VERSION 1.22.0 — committed `f6aed82` on 2026-07-29, *six days after the ISO was
built*. **Proven live in §5: the fresh box ran v1.22.0.**
**3. How the golden reaches a fresh box.** The hub-vouched artifact manifest.
`scripts/felhom-host-install.sh:423-433` `resolve_artifacts()` GETs
`$HUB_URL/api/v1/artifacts/$CUSTOMER_ID` authed with the retrieval passphrase, reads
`golden.version` + `golden.sha256`, then fetches from Gitea and verifies the sha. Nothing baked.
**4. Version pins inside the ISO.** Four candidates; none pins the box's controller or golden:
- `ISO_VERSION="1.25.0"` (`build-felhom-iso.sh:44`) — filename/manifest tag, read by nothing at run time.
- The baked `felhom-bootstrap.sh`**this one IS frozen**; a bootstrap change would require a new ISO.
Currently identical to repo HEAD, so no drift today. This is the single thing that could ever force
an ISO rebuild, and it is the answer to "what is baked".
- `answer.toml`'s disk-setup + throwaway root hash — per-build, not versions.
- The env's two URLs — endpoints, not versions.
**Verdict: no ISO work.** Re-baking + vouching the golden is sufficient, and that is what was done.
## 3. Part 1 — bake, publish, vouch
### 3.1 The version baked, and why not 0.187.0
Part 1 said re-baking to **0.187.0** would level R-120's gate. It would — but it would ship a golden
**without D5**, and Part 2 step 6 *is* the D5 claim, so the proof this task exists for could not have
passed. The operator page also says "rebuilt on the current controller", and current is **0.188.0**.
The brief's arithmetic predates D5 landing (2026-07-30). **Baked `felhom-controller:0.188.0`**, which
also satisfies R-120 (`configs.go:1166` refuses only `compareVersions(golden, newest) < 0`; 0.188.0 vs
the fleet's 0.187.0 is not `< 0`). Recorded here rather than absorbed silently, per standing rule 4.
### 3.2 The bake
Venue: the DooPlex golden-bake nested VM, the **accepted exception for bakes**
(`target-selection.md:59-61`). Reverted to `virgin` first, restored to `virgin` after.
`build-golden.sh v2.1.0`, template `debian-13-standard_13.6-1_amd64.tar.zst`.
Pass markers, as returned: `docker OK (overlay2; data-root /var/lib/docker)`; `including mount point`
for **rootfs, mp0 and mp1** with **no** `excluding` and **no** `FATAL`; archive 619 MB;
`upload OK (HTTP 201)`. Infra images baked = **4**, resolved from the controller itself
(`--print-infra-images`), not the drift-prone fallback list: `traefik:v3.6.7`,
`cloudflare/cloudflared:2026.6.0`, `gtstef/filebrowser:1.3.3-stable`,
`gitea.dooplex.hu/admin/felhom-samba:1.1.0`. Literal-token leak grep on the saved log: **0**.
GOLDEN_VERSION=0.188.0
GOLDEN_SHA256=7353d8beb63641f87a848e45f8aa12e465647e1190ad164a65b32ad01fc3d299
### 3.3 The three observables, as returned
**1 — published.** 404 pre-gate first, with a discriminating control so the check is not vacuous:
golden 0.188.0 pre-gate: HTTP 404
golden 0.186.0 (control): HTTP 200
Then an **anonymous** full download — what a fresh box does, no auth header:
http=200 bytes=649310288
7353d8beb63641f87a848e45f8aa12e465647e1190ad164a65b32ad01fc3d299 golden.tar.zst
Byte count and sha both identical to what the bake printed.
**2 — the manifest vouches it, read back:**
<option value="0.188.0" data-sha="7353d8be…" selected
name="golden_sha256" value="7353d8beb63641f87a848e45f8aa12e465647e1190ad164a65b32ad01fc3d299"
**3 — a consumer resolves it.** The exact call `felhom-host-install.sh:425` makes, against the
**public** hub, passphrase-authed:
GET https://hub.felhom.eu/api/v1/artifacts/sess-g → HTTP 200
{"agent":{"version":"0.118.1","sha256":"9a74c87d…"},
"golden":{"version":"0.188.0","sha256":"7353d8be…"}}
**4 (stronger, from §5) — a real fresh box consumed it:**
[INFO] manifest: agent v0.118.1 (sha 9a74c87dc97f259b…), golden v0.188.0
[INFO] fetching golden v0.188.0 from Gitea → …/vzdump-lxc-9100-2026_07_31-07_40_20.tar.zst
[OK] verified sha256 7353d8beb63641f8… matches the hub manifest
### 3.4 R-120's gate: it evaluated, and it allowed
An absent refusal is not evidence (standing rule 3), so the gate was exercised **both ways** against
the live hub before the real vouch. Vouching golden **0.185.1** (below the fleet's 0.187.0) first:
HTTP/1.1 303 → /configuration?flash=golden_behind_fleet
[WARN] artifact vouch REFUSED: golden 0.185.1 is older than the newest controller the fleet
reports (0.187.0) — a fresh install would land on stale application code (R-120)
and the manifest **still read 0.186.0** afterwards — the refusal wrote nothing. Then the real vouch:
[INFO] Artifact manifest set: agent=0.118.1 golden=0.188.0 min_agent="0.113.0" wrapper_sha=true
**Evaluated and allowed.** For the record and not for action: the gate lives in the **vouch form**
(`hub/internal/web/configs.go:1165`) and cannot fire on an image deploy, so it does not cover every
drift path. Not widened here.
## 4. Part 2 — venue
**demo-hp (t740), Tier 0, the designated drill+build host.** The brief's venue caution is accurate
about the *current* `storage.cfg``local` carries no `images` content type and `local-lvm` is fenced
— but `target-selection.md:38-40` names the fix: *"Put VM disks on a dir storage at `/mnt/nvme-1tb`,
at its root"*, and *"remove scratch storages when done"*. A temporary `cc-scratch` dir storage was
added at that path and removed at teardown (§7). felhom-pve was the alternative and was not chosen:
11 GB free RAM against demo-hp's 25, and it is the Tier 0 box that carries the PBS-DR/offsite tier.
Nested virt confirmed on (`kvm_amd/parameters/nested = 1`). `drill-r50` (VM 300), guest 9201 on both
boxes, `local-lvm`, and both backup targets were untouched.
VM 310: q35/OVMF with `pre-enrolled-keys=0` (the ISO is a `-mkimage` build — **unsigned loader,
Secure Boot must be off**), 8 GB, 4 cores, 150 G qcow2, `boot order=scsi0;ide2` so the empty disk
falls through to the ISO and the installed disk wins afterwards. The ISO already present on demo-hp
was verified byte-identical to the DooPlex build (`99b38420…`).
## 5. Part 2 — the proof, step by step
| # | Step | Result |
|---|---|---|
| 1 | Install from the ISO, real day-0 | **PASS** |
| 2 | Register and claim | **PASS** |
| 3 | Controller is the new one, from the box | **PASS** |
| 4 | Two apps deployed, one with a database | **PASS** |
| 5 | Discriminating data seeded | **PASS** |
| 6 | Back up, restore with `app.yaml` moved aside | **PASS** |
| 7 | Data readable by the app over its real path | **PASS** |
**Step 1 — install.** Unattended from boot to provisioned in **~10 minutes**. PVE 9.2.2 installed,
first-boot stub laid down the retry unit, the box registered itself as an unclaimed appliance
(pairing code `HTG-N8D`), was bound to customer `sess-g` on the hub, and the delivery landed on the
next poll. **The installer that actually ran: `felhom-host-install v1.22.0`** — fetched at run time,
six days newer than the ISO. That is Part 0's claim, proven on a real box.
[OK] Day-0 provision SUCCESS — vmid=9201 host_id=sess-g-e687c3 customer=sess-g
golden=local:backup/vzdump-lxc-9100-2026_07_31-07_40_20.tar.zst
Secrets scrubbed on success: `/etc/felhom/` retained only `.bootstrap-done` and the non-secret
pairing code; `bootstrap.env` and the appliance token were shredded.
**Step 2 — register and claim.** Host enrolled, wg peer registered, host-report accepted, DR-recipe
stored. The claim gate was armed from first boot and the claim was a **real** one — the code exists
only in the customer's email (`R-119`; the operator relayed it). The positive discriminator flipped:
before: {"ok":false,"error":"dashboard not yet claimed"}
after: {"ok":false,"error":"authentication required"}
hub: [INFO] [claim] customer sess-g CLAIMED its dashboard (password set by the customer)
**Step 3 — the controller, from the box, not from the manifest:**
/etc/felhom-controller-image : gitea.dooplex.hu/admin/felhom-controller:0.188.0
docker ps : gitea.dooplex.hu/admin/felhom-controller:0.188.0 | Up (healthy)
felhom-agent --version : felhom-agent 0.118.1
**Step 4 — two apps, through the real endpoints** (`GET /stacks/{app}/deploy` for the pre-generated
values, then `POST /api/stacks/{app}/deploy`, the endpoint the UI invokes):
- **rallly** — `lukevella/rallly:4.11.1` + `postgres:16-alpine`, the DB app. Chosen because its
`DB_PASSWORD` is `type: secret`, i.e. exactly D5's travelling class, and its `DATABASE_URL` is a
TCP URL to a *different container*: `postgresql://rallly@rallly-postgres:5432/rallly`.
- **homebox** — sqlite, no DB container.
Both healthy; rallly served 200 through traefik on its own route.
**Step 5 — the discriminator.** `cc_proof` table, one row **before** the backup
(`PRE-BACKUP-sess-g-2026-07-31`), one **after** (`POST-BACKUP-sess-g-MUST-NOT-RETURN`). Without this
pair a stale restore is indistinguishable from a good one.
**The read path, established and red-proofed before it was trusted.** All DB access ran from
**rallly's own network namespace** (`docker run --network container:rallly … psql -h rallly-postgres`)
— the path `DATABASE_URL` names, never `127.0.0.1` inside the postgres container, which postgres
trusts and which produced D5's §1.2 false pass. Proven to be doing real work:
correct password : rallly|t|t (user=rallly, over_tcp=t, PostgreSQL 16)
wrong password : FATAL: password authentication failed for user "rallly"
**Step 6 — backup, then restore with the guest gone.**
[backup] Recovery unit captured for rallly → /mnt/sys_drive/felhom-data/backups/primary/rallly
(images=2, secrets-referenced=2, data_keys=0, portable-carried=2/2, withheld=0)
[backup] Volume dump: rallly/rallly_rallly_postgres_data → 65.0 MB
Unit `compose/app.yaml`: mode **0600**, header naming the carried class, both secrets present.
Manifest `schema_version: 2`, `controller_version: 0.188.0`. The carried `DB_PASSWORD` was verified
**by fingerprint, never printed**, against the live one:
unit DB_PASSWORD sha256=14c8f5152b205829
live DB_PASSWORD sha256=14c8f5152b205829
and the guest's own copy was `ENC:` at rest. The guest's `app.yaml` was then **moved aside**
confirmed absent — and the restore run through `POST /backup/restore`:
[backup] Restoring rallly from recovery unit: images=2, secrets recovered=2/2, data_keys=0
[backup] Restored 1 Docker volume(s) for rallly
[backup] Restore rallly: replaying DB dump into rallly-postgres (postgres)
[backup] Restore-from-unit completed: rallly (30.97 s)
**2/2 with the guest's app.yaml gone.**
**Step 7 — the application reading its own data.** The **live app process** was asked for its own
credential, and it matches the pre-backup one — so the secret genuinely survived the guest:
host=rallly-postgres port=5432 user=rallly pw_sha256=14c8f5152b205829
Then the read, over that path:
current_user | server_ip | over_tcp
rallly | 172.19.0.2/32 | t
id | marker
1 | PRE-BACKUP-sess-g-2026-07-31
PRE-BACKUP rows = 1
POST-BACKUP rows = 0
**The discriminator held.** 32 `public` tables intact; the guest's `app.yaml` was regenerated from the
unit with both secrets re-encrypted at rest; rallly's own healthcheck returned **200**, and traefik
served `poll.sess-g.test` **200**. Checked the data, not the exit code.
One difference from D5's adventurelog run worth recording: this unit carried **both** a `.sql` dump
and the volume tar, and the restore did both (volume restored, then dump replayed). D5's case had
`db_dumps: None`. The password mattered either way — the replay authenticates with it.
## 6. Part 3 — the runbook integrity fix
`RUNBOOK-manual-build.md:104-106` told the reader to *"use its RECORDED qemu launch line; do not
reconstruct from memory"*, citing `RUNBOOK-publish-0.79-0.110-2026-07-10.md:29` — a line labelled
**"PASS (reconstructed — DEVIATION)"** whose own §Deviations says *"the canonical `qemu-system`
one-liner was never saved"*. The document forbade improvising and pointed at an improvisation.
**Captured verbatim from this bake and recorded as canonical** in `RUNBOOK-manual-build.md` §4.0:
```bash
qemu-img snapshot -a virgin /mnt/5_hdd/felhom.eu/drill/drill.qcow2
qemu-system-x86_64 -enable-kvm -cpu host -smp 4 -m 8192 \
-drive file=/mnt/5_hdd/felhom.eu/drill/drill.qcow2,format=qcow2,if=virtio,cache=writeback \
-netdev user,id=n0,dhcpstart=10.0.2.30,hostfwd=tcp::2222-10.0.2.15:22 \
-device virtio-net-pci,netdev=n0 -display none -daemonize \
-pidfile /mnt/5_hdd/felhom.eu/drill/qemu.pid
```
SSH on `:2222` in ~40 s; `pve-manager/9.2.2`. §4 also now carries the bake/publish/teardown steps and
the template-rot warning, and the old runbook's deviation entry is marked SUPERSEDED with a forward
pointer so the reconstruction is never used as an authority again.
## 7. Teardown — all three layers
1. **The machine.** VM 310 stopped and `qm destroy --purge --destroy-unreferenced-disks 1`.
`qm list` afterwards shows only `drill-r50` (300), untouched; `/mnt/nvme-1tb/images/` is empty.
2. **The host.** `cc-scratch` removed; `storage.cfg` back to its original four entries.
`felhom-backup` available **926 492 284 KiB before the run and 926 492 284 KiB after** — the space
returned exactly.
3. **The hub. `sess-g` and its host record are DELETED — the full cascade ran.** See §7.1.
### 7.1 Layer 3, stated explicitly
The delete gate **refused twice before it allowed**, which is the gate working, and both refusals are
recorded because "I deleted it" without them would hide that this needs a wait:
POST /hosts/sess-g-e687c3/delete → 409 Conflict (host ONLINE)
POST /configs/sess-g/delete (bare confirm) → 400 (all three acknowledgements required)
`customer_delete.go:220-227` refuses while `hostStatus(...) == "ok"`, so the record cannot be deleted
until the destroyed box ages out of ONLINE (~30 min after its last report). It flipped `ok → stale` at
08:25, and the cascade then ran:
POST /configs/sess-g/delete
ack_hosts=1 ack_reset=1 ack_purge=1 confirm_id=sess-g expect_hosts=1
→ 303 /configs?flash=deleted
[INFO] customer DELETE cascade started for sess-g (journal #8, 1 host(s))
[INFO] delete sess-g: host sess-g-e687c3 deleted (escrow DEMOTED to retained custody)
[INFO] tenantsync: deprovision ok for sess-g (ns=sess-g, existed=false)
[INFO] delete sess-g: residue purged (reports=5 app_telemetry=7 notif_prefs=1
selfbind_tokens=1 appliance_registrations=1)
[INFO] customer DELETE cascade COMPLETE for sess-g (journal #8) — full teardown
**Verified positively, not by absence** — both pages loaded with real content and returned zero hits,
and the unclaimed-appliance row is gone by its own UUID:
/configs loaded 10160 bytes, occurrences of 'sess-g' = 0
/hosts loaded 9880 bytes, occurrences of 'sess-g' = 0
/hosts occurrences of appliance uuid f9f5d7e0-… = 0
**A polling trap, recorded because it nearly produced a false result here.** The first delete-gate
poll loop broke on `grep -q '"status":"ok"' || break` — and an **empty** response satisfies that just
as well as a status change. It fired on one transient empty reply and reported "GATE OPEN" while the
host was still ONLINE. Re-run parsing the JSON and treating unparseable as *retry*, the host was
`ok` for another four minutes. Standing rule 3, in miniature: the absent signal is not the positive one.
**`sess-f` is NOT deleted** — it belongs to the 2026-07-30 session, not this run (R-131). Command for
whoever picks it up, once its host is not ONLINE:
POST /configs/sess-f/delete
ack_hosts=1 ack_reset=1 ack_purge=1 confirm_id=sess-f expect_hosts=<live host count>
**Secrets.** Shredded in the guest (`rallly-dbpw`, the moved-aside `app.yaml`, deploy bodies, session
and CSRF files, claim artefacts) and on the box; helper scripts removed from both. The scratch
customer's retrieval passphrase and the ISO's throwaway root password were handled file→file and
never printed; both die with the session scratchpad and the destroyed VM.
## 8. Findings — filed, none fixed
- **R-128** — `build-felhom-iso.sh:44` claims `ISO_VERSION` "aligns with felhom-host-install
SCRIPT_VERSION". ISO 1.25.0, host-install 1.22.0. A comment asserting an invariant nothing
maintains. Cosmetic; it misleads a reader about which installer an ISO implies.
- **R-129** — the docs say demo-hp has **"no baked SSH key"** and needs the G1 break-glass password
(`CLAUDE.md`, `target-selection.md:41-42`, `felhom-agent/CLAUDE.md`). `ssh -o BatchMode=yes demo-hp`
authenticated by **key**, first try. The prohibition is stale in a direction that costs a future
session a needless vault round-trip.
- **R-130** — a fresh box's `local-lvm` was ~75 GiB against `HARD_MIN_LVM_GIB=120`, and the installer
logged `[WARN] local-lvm free ~75 GiB < hard min 120 GiB` and **continued to a fully successful
install**. Either the hard minimum is not hard, or it is wrong. A "hard min" that warns is the
R-29 shape.
- **R-131 (housekeeping)** — hub customer **`sess-f`** ("R-120 golden 0.186.0 proof", DOWN) is a
scratch customer left behind by the 2026-07-30 session — the fourth instance of the accumulation
`target-selection.md:86-87` and `PROMPT-TEMPLATE.md` §13 warn about (`drill-r50`, `sess-c`,
`sess-d`, now `sess-f`). Not deleted here: it is not this run's record. Command in §7.1.
- **R-132 — `curl -w '%{redirect_url}'` reconstructs the request URL WITH its basic-auth credential.**
While red-proofing the R-120 gate, a call made correctly with `-u ":$HUB_PW"` (no credential in any
URL) still printed the hub operator password into this session's transcript, because the *write-out
format* rebuilds the URL from the credential curl was given. The request was safe; the reporting was
not. **`HUB_PW` should be rotated.** Rule going forward: read a redirect from `-D -` and
`grep '^Location:'`, never `%{redirect_url}`, on any authenticated call.
- **R-119 confirmed live**, not re-filed: the claim code is emailed-only with no operator-side route,
and it is the single human step in an otherwise unattended day-0. Budget for it before a run.
## 9. §7.2 — the sentence that matters
**Yes — a fresh install is safe to hand to an external tester.** From ISO boot to a claimable,
app-serving box was ~10 minutes unattended; the box fetched the current installer and the newly
vouched golden and verified both by sha; two apps deployed through the real endpoints; and an app's
data came back from the drive with the guest's `app.yaml` gone, readable by the application over its
own TCP path, with a discriminator proving it was a restore and not a no-op.
**One thing the tester's host must do that the operator must plan for:** the claim code arrives only
by email (R-119), so the tester needs a working mailbox on the customer record before the box boots.
Nothing else found here blocks a tester; R-128/R-129/R-130 are documentation and gate-hygiene issues,
and R-131 is housekeeping.
**What "safe" does not mean:** bug-free. Two things this run did **not** cover — the offsite/PBS-DR
legs (deliberately: the task forbids pointing anything at production PBS or the real restic offsite,
so `sess-g` was created with DR tier off and offsite disabled), and any bare-metal ISO. Only a
`nested-vm`-profile v1.25.0 ISO exists on disk; a tester on real hardware needs a `generic`- or
`hp`-profile build from the same tooling, which is an ISO build and therefore its own task.
## 10. What did not happen, and why
- **No ISO built** — Part 0 said none was needed, and the brief says to stop and report if one were.
- **No defect fixed** — R-128/129/130/131 filed only; a fix mid-run would describe a version that
never existed.
- **No existing box changed** — no golden deployed to 9201 on either demo host, no floor moved
(still v0.156.0), no MinAgent change (still 0.113.0).
- **Campaign 10, the demo-hp repartition, subdomain onboarding** — out of scope, untouched.
- **`sess-f` not deleted** — see R-131.
+5
View File
@@ -70,6 +70,11 @@ State: `BLOCKED` · `READY` · `WAITING-ON-OPERATOR` · `WATCHING`. Every row ha
| **R-89** | Retention as a per-customer **commercial** policy on the hub | READY (increment 2) | — | Policy object + reconciler → ep0 prune job; keep box tokens write-only | CC |
| **R-92** | Hub PBS-DR gauge is 0.1 GB-granular — small deltas unverifiable | READY (XS) | — | Widen precision when retention becomes customer-visible | CC |
| **R-93** | `drill-r50` is both a blocked customer and the only drift fixture | READY (XS) | — | Retire it for a synthetic fixture, or unblock + silence per-customer | CC |
| **R-128** | `build-felhom-iso.sh:44` comments that `ISO_VERSION` "aligns with felhom-host-install SCRIPT_VERSION" — ISO is **1.25.0**, host-install is **1.22.0** | READY (XS) | — | A comment asserting an invariant nothing maintains, in a file whose whole job is to say what an ISO implies. Fix = drop the claim, or make the build read `SCRIPT_VERSION` and refuse a mismatch. Found during the 2026-07-31 tester-gate run (`audits/tester-gate-golden-0.188.0-2026-07-31.md` §2) | CC |
| **R-129** | **Every doc says demo-hp has "no baked SSH key"** and needs the G1 break-glass password — but `ssh -o BatchMode=yes demo-hp` authenticated **by key**, first try, 2026-07-31 | READY (XS) | — | Stale in the expensive direction: a session that believes it sends itself to the hub vault for a credential it does not need. Verify who owns the key and when it landed, then correct `CLAUDE.md`, `runbooks/target-selection.md:41-42`, `runbooks/workspace-CLAUDE.md` and `felhom-agent/CLAUDE.md` together — or remove the key if it was not deliberate | CC |
| **R-130** | **A "hard min" that only warns.** A fresh box's `local-lvm` was ~75 GiB against `HARD_MIN_LVM_GIB=120` (`scripts/felhom-host-install.sh`); the installer logged `[WARN] local-lvm free ~75 GiB < hard min 120 GiB` and went on to a **fully successful** install | READY (S) | — | Either the minimum is not hard (rename it and state the real floor) or it is wrong (and 120 GiB is not what a working appliance needs). Leaving it is the R-29 shape: a check that reads as coverage while providing none. Evidence: same audit §8 | CC |
| **R-131** | **`sess-f` is a fourth orphaned scratch customer** on the hub ("R-120 golden 0.186.0 proof", DOWN), left by the 2026-07-30 session | READY (XS) | — | After `drill-r50`, `sess-c`, `sess-d` — the accumulation `runbooks/target-selection.md:86-87` and `PROMPT-TEMPLATE.md` §13 both warn about, now on its fourth instance. Delete it (see the recorded command in `audits/tester-gate-golden-0.188.0-2026-07-31.md` §7.1); the recurrence itself argues for a periodic scratch-customer sweep rather than another reminder | CC |
| **R-132** | **`curl -w '%{redirect_url}'` reconstructs the request URL WITH its basic-auth credential** — so a `-u ":$HUB_PW"` call that never put the password in a URL still printed it | **ACTION: rotate `HUB_PW`** | — | Happened on 2026-07-31 while red-proofing the R-120 gate: the hub operator password was written to the session transcript by the write-out format, not by the request. `-u` is safe; the *reporting* was not. Rule: read the redirect from `-D -` and grep `^Location:`, never `%{redirect_url}`, on any authenticated call. Rotate the hub password (`/configuration` → Login password; ConfigMap `auth.password_hash` is the reset path) and update `~/.config/credentials` | Viktor |
| **R-133** | **The vaulted break-glass console credential is PLAINTEXT AT REST — every hub DB backup is a fleet-wide console-credential dump.** `host_recovery.secret` holds each managed box's `root@pam` password verbatim, so any copy of the SQLite DB (Longhorn snapshot, PBS backup of the hub PVC, a hand-taken copy during a diagnosis) carries root console access to every Felhom host in one file | **READY (M) — NEW 2026-07-31** | — | **The deferred leg of hub v0.84.0** (Console access card), filed separately because v0.84.0 changed only WHO can retrieve the secret, never how it is stored. v0.84.0 makes it more worth doing, not more broken: retrieval now rides the hub SESSION, so the DB and the login password are jointly the whole protection (ruling **S-4**, `CONTEXT.md`). Fix shape: **envelope-encrypt the `host_recovery.secret` column under a KEK held outside the DB** — the hub already proves it can hold something it cannot itself read (escrow blobs), and that contrast is the argument. Two constraints the design must respect: the credential must stay retrievable **when the box is unreachable** (that is the whole point of break-glass), so the KEK cannot live on the box or depend on the agent; and the global-key API path must keep working with the hub UI down. Would flip the capability-map row **"Break-glass management-plane recovery"**, which today reads IMPLEMENTED with this as its caveat | CC |
## Why the TOP READY rows rank this way
@@ -77,6 +77,10 @@ unless/until a managed agent_update is triggered.
- **Phase C drill launch RECONSTRUCTED** (operator-authorized): the canonical `qemu-system` one-liner was
never saved; reconstructed from the audits' parameters (recorded above); virtio bus + slirp/hostfwd
booted cleanly first try; virgin snapshot reverted afterwards, exactly as found.
> **SUPERSEDED 2026-07-31 — do not use the line above as an authority.** It is a reconstruction, and
> `RUNBOOK-manual-build.md` used to cite it while telling the reader not to improvise. The canonical
> invocation is now captured from a real bake and lives in **`runbooks/RUNBOOK-manual-build.md` §4.0**.
> This entry stays only as the historical record of the deviation.
- **Phase E qualified:** the demo was already on 0.110.0 (C2 task), so the floor-driven self-update proof
isn't demonstrable on it; the anon-fetch Day-0 dry proof stands in (a stronger fetchability proof).
+59 -10
View File
@@ -100,19 +100,68 @@ on every box below it, on their next report.
## 4. Golden image (fresh Day-0 installs)
The golden is a pre-baked controller-era guest image built in the **drill VM** on 180 — procedure per
`RUNBOOK-publish-0.79-0.110-2026-07-10.md` Phase C (use its RECORDED qemu launch line; do not
reconstruct from memory):
The golden is a pre-baked controller-era guest image built in the **drill VM** on 180
(`/mnt/5_hdd/felhom.eu/drill/drill.qcow2`, internal snapshot `virgin`).
1. Revert the drill VM to its `virgin` internal snapshot; boot it with the recorded qemu one-liner.
2. Run `build-golden.sh <controller VER>` (agent repo `configs/`) — bakes
`felhom-controller:<VER>` as a transient unit, uploads the golden to Gitea packages (404 pre-gate
applies), prints the **sha256 — record it**.
3. Teardown: purge the build guest, shred the token/script/log, revert to `virgin`, and grep the
transcript for token leaks (must be 0 hits).
4. Hub → Configuration → Day-0 artifacts: pick the new Golden, Save.
### 4.0 The canonical drill-VM launch — captured from a real bake, not reconstructed
Until 2026-07-31 this section told the reader to "use the RECORDED qemu launch line" from
`RUNBOOK-publish-0.79-0.110-2026-07-10.md` Phase C — while that line is itself labelled
**"PASS (reconstructed — DEVIATION)"** and its own §Deviations says *"the canonical `qemu-system`
one-liner was **never saved**"*. The document forbade improvising and pointed at an improvisation. The
lines below were **captured verbatim from the 0.188.0 bake on 2026-07-31** and are now the canonical
invocation. Re-capture them (do not retype from memory) if the bake host or disk layout ever changes.
```bash
# 1. revert the disk to virgin (non-destructive to the snapshot; succeeding also proves no qemu holds the qcow2)
qemu-img snapshot -a virgin /mnt/5_hdd/felhom.eu/drill/drill.qcow2
# 2. COLD-boot it (the snapshot is disk-only, 0 B VM_SIZE — never -loadvm)
qemu-system-x86_64 -enable-kvm -cpu host -smp 4 -m 8192 \
-drive file=/mnt/5_hdd/felhom.eu/drill/drill.qcow2,format=qcow2,if=virtio,cache=writeback \
-netdev user,id=n0,dhcpstart=10.0.2.30,hostfwd=tcp::2222-10.0.2.15:22 \
-device virtio-net-pci,netdev=n0 -display none -daemonize \
-pidfile /mnt/5_hdd/felhom.eu/drill/qemu.pid
```
`if=virtio` is load-bearing (the guest expects `/dev/vda`). SSH answers on `:2222` in ~40 s
(`ssh -i /mnt/5_hdd/felhom.eu/drill/id_drill -p 2222 root@localhost`); `pveversion` read
`pve-manager/9.2.2` on 2026-07-31. **Liveness check:** `ps -eo comm | grep qemu-system-x86`
`pgrep -f qemu-system-x86_64` self-matches your own command line and reports a false "still running".
### 4.1 Bake + publish
1. Revert + boot per §4.0.
2. The debian template is **absent on `virgin`** and **the exact point release rots** — list the
current one (`pveam available --section system | grep debian-13`) and `pveam download local <that>`.
It was `debian-13-standard_13.6-1_amd64.tar.zst` on 2026-07-31.
3. `scp` in `build-golden.sh` (agent repo `configs/`) + the Gitea token (`~/.gitea-token` on 180,
0600), then run it as a transient unit so it survives a session close, reading the token from the
file **inside** the VM so it never reaches a command line:
```bash
GT=$(cat /root/.gitea-token); systemd-run --unit=golden-bake --collect \
--setenv=GITEA_USER=admin --setenv=GITEA_TOKEN=$GT \
--setenv=REGISTRY_USER=admin --setenv=REGISTRY_TOKEN=$GT \
bash -c "/root/build-golden.sh 9100 local:vztmpl/<template> local-lvm local vmbr0 \
gitea.dooplex.hu/admin/felhom-controller:<VER> > /root/bake.log 2>&1"
```
`CONTROLLER_IMAGE` is a **required** argument (a hand-bumped default rotted twice) and
`GOLDEN_VERSION` is derived from it — the golden's version IS the controller it bakes.
**The script is the publisher**: it uploads to Gitea and prints `GOLDEN_VERSION` + `GOLDEN_SHA256`.
Pass markers: `overlay2 OK`, `including mount point` for rootfs **and mp0 and mp1** with no
`excluding`/`FATAL`, `upload OK (HTTP 201)`. The 404 pre-gate applies to the package URL.
4. Teardown: `pct destroy 9100 --purge`, `shred -u` the token/script/log **after** copying the log out
for evidence, `poweroff`, wait for qemu to exit, `qemu-img snapshot -a virgin`. Token-leak grep on
the saved log = `grep -c -F "$(cat ~/.gitea-token)"` (the literal value — a broad `[a-f0-9]{40}`
pattern false-hits image shas).
5. Hub → Configuration → Day-0 artifacts: pick the new Golden, Save. **The R-120 gate lives on this
save** (`hub/internal/web/configs.go:1165`) and REFUSES a golden older than the newest controller
the fleet reports. It does **not** run on a controller image deploy — it is not a general drift net.
A golden is only needed when a publish train wants fresh installs current — demo deploys never need it.
The full 0.188.0 run, with the observables: `documentation/audits/tester-gate-golden-0.188.0-2026-07-31.md`.
## 5. Hub (felhom.eu/hub → k3s, GitOps)