15397468df
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
5.7 KiB
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 bygit fetch), versionv0.13.1(ldflags-injected; no source constant) → targetv0.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
8098237—hub 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 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; missingcustomer_id→400. - store
TestGetHostByCustomer— none→nil, one→hit, two→most-recently-updated.
- A — first enroll mints →
- 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 newhost_idc1-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 ./...— allok(api, configgen, monitor, store), no failures.
Deploy + pod verification
./build.sh 0.14.0 --pushon the build box (auto-pulls8098237) → imagegitea.dooplex.hu/admin/felhom-hub:0.14.0pushed (digestsha256:541cdb54…, 24M).kubectl set image deploy/hub→ rollout succeeded; podhub-6c7d8bcc9-454n91/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 <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_reportsrows → counts 0/0/0 (verified). - Real
demo-felhomcustomer confirmed still present / untouched. - Uninstalled temporary
sqlitefrom 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/hostsis 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/hostsre-mint still orphans a prior host (no idempotency there); option C fixes this structurally for the enroll path.