hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)

New POST /api/v1/host-enroll (handleHostEnroll): X-Retrieval-Password authed,
body {customer_id} -> {host_id, api_key}. Mint-once-reuse (201 first, 200
reuse) so re-running the host-bootstrap never orphans a running agent's key;
auth checked before any mint. Backed by new Store.GetHostByCustomer
(ORDER BY updated_at DESC LIMIT 1, idx_hosts_customer).

GET /config/{id} and global-key POST /admin/hosts left untouched. Exact-match
route (path == "/host-enroll") to avoid the /hosts/ prefix collision.

Tests: host_enroll_test.go (mint/reuse/401-no-mint/404/400) + GetHostByCustomer
store test; companion red-proof verified always-mint fails the reuse assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
2026-06-26 15:35:24 +02:00
parent 230980f7a8
commit 8098237ce1
7 changed files with 331 additions and 1 deletions
+25
View File
@@ -1,5 +1,30 @@
# Felhom Hub — Changelog
## v0.14.0 — Passphrase-authed host enrollment (Day-0 option C) (2026-06-26)
Adds the single-secret Day-0 host-enrollment path proven in
`documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md` (option C). The operator /
host-bootstrap script now carries **only the customer's retrieval passphrase** — the global operator
key never enters the field deploy path.
- **New endpoint `POST /api/v1/host-enroll`** (`internal/api/handler.go`, `handleHostEnroll`):
passphrase-authed (`X-Retrieval-Password` header, body `{customer_id}`), returns
`{host_id, api_key}`. **Mint-once-reuse** — mints on first call (`201`), returns the existing
credential byte-for-byte on every subsequent call (`200`), so re-running the bootstrap never orphans
a running agent's key. Auth is checked **before** any mint (a wrong passphrase never writes a row):
wrong/missing passphrase → `401`, unknown customer → `404`, missing `customer_id``400`. Mirrors
`handleConfigRetrieve`'s auth pattern + `handleAdminCreateHost`'s mint block.
- **New store method `Store.GetHostByCustomer`** (`internal/store/store.go`): `SELECT … FROM hosts
WHERE customer_id = ? ORDER BY updated_at DESC LIMIT 1` (uses `idx_hosts_customer`), nil-on-not-found.
Backs the reuse lookup. >1 host for a customer (not expected in Day-0) → most-recent wins, never a
duplicate mint.
- **Unchanged & deliberately untouched:** `GET /api/v1/config/{id}` (controller pull — same raw-YAML
body) and `POST /api/v1/admin/hosts` (global-key operator escape hatch, still PROVISIONAL pending the
cutover lock-down).
- Tests: `internal/api/host_enroll_test.go` (mint/reuse/401-no-mint/404/400, +5) and a
`GetHostByCustomer` store test (+1); companion red-proof confirmed an always-mint variant fails the
reuse assertion.
## v0.13.1 — DR recipe v1 drive-shape sync: test-data + regression guard only (2026-06-16)
**No behavior change — redeploy optional.** Tracks the agent's v0.39.0 v1 host-half drive shape (which