From 15397468df321a241461efd5dc07dce4673857ab Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 26 Jun 2026 15:39:28 +0200 Subject: [PATCH] =?UTF-8?q?REPORT:=20hub=20v0.14.0=20host-enroll=20?= =?UTF-8?q?=E2=80=94=20shipped=20+=20live-verified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-test results + companion red-proof, deployed v0.14.0 pod verification, live throwaway enroll/reuse/401/404/400 + Bearer-auth check, cleanup confirmed. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7 --- REPORT.md | 109 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 35 deletions(-) diff --git a/REPORT.md b/REPORT.md index f73ab6a..9e282ab 100644 --- a/REPORT.md +++ b/REPORT.md @@ -4,44 +4,83 @@ --- -## REPORT — hub v0.13.1: DR recipe v1 drive-shape sync (test-data + regression guard only) +## hub v0.14.0 — Passphrase-authed host enrollment (Day-0 option C) — 2026-06-26 -**TASK — DR-recipe completion (hub side of a cross-repo change).** The agent's **v0.39.0** dropped two -fields from the host-half `drives[]` (`role`, `restic_repo_coord`) and now resolves the `pbs` coord live. -The hub change is **test-data + a regression guard only — NO behavior change; redeploy optional**, because -the hub already reads drive sections as `json.RawMessage` (verbatim passthrough): dropping fields needs no -store/handler/struct change. +**Task:** Implement option C from `documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md`: +a passphrase-authed, mint-once-reuse host-enrollment endpoint so Day-0 carries **one** secret (the +retrieval passphrase) and the global operator key never enters the field deploy path. Single repo +(`felhom.eu`, `hub/`). -**Baseline:** v0.13.0 (`5f5e3c5`) → **v0.13.1** (`149a3b0`, pushed to `main`). +### Confirmed baseline +- felhom.eu (hub) `main` @ `230980f` (= origin/main, verified by `git fetch`), version `v0.13.1` + (ldflags-injected; no source constant) → target `v0.14.0`. +- Reconfirmed every relied-on symbol live: `handleConfigRetrieve` (auth pattern), `handleAdminCreateHost` + (mint block — left untouched), `UpsertHost` (`ON CONFLICT(host_id)`), `GetHostByAPIKey`/`GetHost`/ + `scanHost`/`hostSelectCols`, `idx_hosts_customer`, `configgen.RandomHex`. -### Changes -- `internal/store/testdata/dr-recipe.golden.json` + the `drHostHalf` test fixture — dropped the `role` - key from `drives[0]` to match the v1 shape the agent emits. -- `internal/api/testdata/host-report.golden.json` — re-synced **byte-identical** with the agent's - `internal/hub/testdata/host-report.golden.json` (sha256 `57f2a5e7…18b2f2b5`). The hub copy previously - lacked the `dr_recipe` section entirely; it is now a verbatim copy, so the cross-repo golden truly - matches and POSTing it through `/host-report` exercises the `SaveDRRecipeHostHalf` ingest path. -- New `internal/store/dr_recipe_test.go::TestAssembleDRRecipe_V1DriveShape` — the regression guard: a - stored host half whose `drives[]` carry NEITHER dropped field but WHICH HAS a `pbs` block assembles - cleanly (pbs carried through, drives passed through verbatim, neither `role` nor `restic_repo_coord` - present). Demonstrated to FAIL when the fixture re-adds `role`, then reverted. +### Files changed +| File | Change | +|------|--------| +| `hub/internal/store/store.go` | **+** `Store.GetHostByCustomer` (SELECT … WHERE customer_id=? ORDER BY updated_at DESC LIMIT 1; nil-on-not-found; uses `idx_hosts_customer`) | +| `hub/internal/store/host_test.go` | **+** `TestGetHostByCustomer` (none→nil / one / two→most-recent / other-customer) | +| `hub/internal/api/handler.go` | **+** route `POST /host-enroll` (exact-match, beside `/host-report`); **+** `handleHostEnroll` (passphrase auth → mint-once-reuse). `handleConfigRetrieve` + `handleAdminCreateHost` **unchanged** | +| `hub/internal/api/host_enroll_test.go` | **+** 5 scenario tests (mint/reuse/401-no-mint/404/400) | +| `hub/CHANGELOG.md` | **+** v0.14.0 entry (newest on top) | +| `hub/README.md` | **+** "Host Enrollment (Day-0, option C)" section | +| `documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md` | **+** "c-pref SHIPPED in v0.14.0" pointer in §5 | -### Verification -- `go build ./... && go vet ./... && go test ./...` — **green**. -- **Live (the real acceptance):** after deploying the daemon (agent v0.39.0) and its startup host-report - landing in the hub DB, the **real `AssembleDRRecipe`** over a copy of the live `/data/hub.db` produced - the `demo-felhom` recipe — `recipe_version 1`, 3 guests w/ sizing, **`pbs` present** (`felhom-pbs/root/9201`), - **2 drives clean** (no `role`/`restic_repo_coord`), 5 pve_storage, **14 apps**. **Secret scan over the - full assembled recipe AND both raw stored halves = 0 hits** (nothing to redact — the point of the arc). - (Operator HTML panel not browser-rendered: no dashboard password in this env; the server pipeline + - exact stored/assembled bytes were verified.) +### Commits +- `8098237` — `hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)` (code + tests + CHANGELOG + README + spike pointer). +- `` — REPORT.md overwrite. -### Cross-repo golden checksum -`hub/internal/api/testdata/host-report.golden.json` ≡ `felhom-agent/internal/hub/testdata/host-report.golden.json`: -`57f2a5e7154ece1be94a79fd327f2924762e70c3ee388d6e375df7c918b2f2b5` (both). +### Tests (temp-sqlite stores; no network) +- **Per-scenario (all PASS):** + - A — first enroll mints → `201`, `host_id=c1-<6hex>`, `api_key=64hex`, exactly 1 host row, `GetHostByAPIKey(minted)` resolves. + - B — second enroll **reuses** → `200`, creds **byte-for-byte identical**, still exactly 1 host row. + - C — wrong passphrase → `401`, host-row count **unchanged at 0** (auth-before-mint proven). + - D — unknown customer → `404`, no mint. + - E — missing header → `401`; missing `customer_id` → `400`. + - store `TestGetHostByCustomer` — none→nil, one→hit, two→most-recently-updated. +- **Companion red-proof (§10):** temporarily disabled the reuse branch (always-mint) → Scenario B + **FAILED** as required (`second status = 201, want 200` — second call returned a *new* `host_id` + `c1-0d44c6`, not the first). Reverted to mint-once-reuse → green. This proves the idempotency test is + load-bearing, not hollow. +- **Test count:** api package 29 → **34** (+5); store package 11 → **12** (+1). +- **Green gate:** `go build ./... && go vet ./... && go test ./...` — all `ok` (api, configgen, monitor, + store), no failures. -### BACKLOG flagged (not acted on) -External-drive bulk data has **no offsite / second-failure-domain backup** — cross-drive (Tier-2) rsync -lands on the same internal SSD in the same guest. This is why `restic_repo_coord` named nothing real and -was dropped from v1. Design a genuine off-host bulk-data tier before re-introducing a drive backup coord. -Full detail in `felhom-agent/REPORT.md` §8. +### Deploy + pod verification +- `./build.sh 0.14.0 --push` on the build box (auto-pulls `8098237`) → image + `gitea.dooplex.hu/admin/felhom-hub:0.14.0` pushed (digest `sha256:541cdb54…`, 24M). +- `kubectl set image deploy/hub` → rollout succeeded; pod `hub-6c7d8bcc9-454n9` `1/1 Running`. +- Startup log: `[INFO] felhom-hub 0.14.0 starting`; deployed image confirmed `…:0.14.0`. + +### Live verification against the real endpoint (throwaway `enroll-spike-2026-06-26`) +Curl-against-real-endpoint (the exact call the future host-bootstrap script makes). Secrets stayed +resident on the build box; only statuses / non-secret host_id / identical-bytes booleans surfaced. + +| Check | Result | +|-------|--------| +| 1. first enroll (correct passphrase) | **201**, `host_id=enroll-spike-2026-06-26-c641d7`, api_key 64-hex | +| 2. second enroll (idempotent) | **200**, host_id identical YES, api_key identical YES, **exactly 1 host row** | +| 3. wrong passphrase | **401** | +| 4. unknown customer | **404** | +| 5. missing customer_id | **400** | +| 6. **Bearer-auth proof** — `GET /hosts/{host_id}/desired-state` with `Authorization: Bearer ` | **200** (the enrolled credential is a real, working host key end-to-end) | + +### Cleanup (throwaway discipline, mirroring the spike) +- Deleted throwaway `customer_configs` / `hosts` / `host_reports` rows → counts **0/0/0** (verified). +- Real `demo-felhom` customer confirmed **still present / untouched**. +- Uninstalled temporary `sqlite` from the hub pod (`apk del`); removed all `/tmp/enroll-spike*` scratch. +- The global key was read out-of-band into a shell var only; **no secret** written to disk, any + committed file, or this report. + +### Observations / out-of-scope (not acted on) +- `POST /admin/hosts` is still **PROVISIONAL** (`handler.go:595`) and is left **as the operator/global + escape hatch** — its removal/lock-down is a separate cutover decision, deliberately not done here. +- `GET /config/{id}` was **not** modified (no host fields, no JSON, raw-YAML body intact) — the in-guest + controller pull is unaffected. +- The host-bootstrap script (Day-0 wiring) is the **next** task — not started here. `host-enroll`'s + mint-once-reuse is the idempotency primitive that step will depend on. +- Under option A, `admin/hosts` re-mint still orphans a prior host (no idempotency there); option C + fixes this structurally for the enroll path.