Files
felhom.eu/REPORT.md
T
admin 15397468df REPORT: hub v0.14.0 host-enroll — shipped + live-verified
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
2026-06-26 15:39:28 +02:00

5.7 KiB

felhom.eu — task reports

Overwrite this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in hub/CHANGELOG.md.


hub v0.14.0 — Passphrase-authed host enrollment (Day-0 option C) — 2026-06-26

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/).

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.

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

Commits

  • 8098237hub v0.14.0: passphrase-authed host enrollment (Day-0 option C) (code + tests + CHANGELOG + README + spike pointer).
  • <this REPORT commit> — REPORT.md overwrite.

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 reuses200, 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_id400.
    • 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.

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 proofGET /hosts/{host_id}/desired-state with Authorization: Bearer <enrolled key> 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.