# 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/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 - `8098237` — `hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)` (code + tests + CHANGELOG + README + spike pointer). - `` — 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`; 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. ### 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.