gates / gates (push) Successful in 6m44s
- Configuration → Access tokens: named, scoped (group read/write), expiring (≤365 d), shown once, stored SHA-256 only, every use logged by name. Bearer fhp_… on the web surface only; /api/v1 refuses it. - Route table = the code's single source (web/routes.go), pinned to ServeHTTP's switch; session-only: secrets shown/minted, deletes/resets, password, tokens. - R-952 closed: /api/v1 route table names each caller, pinned to the switch; no-credential refusal test. - Fail-open fixed: an empty operator password served the UI open (main skipped RequireAuth). Live hub never open. - 05 §17, 04 §6.2, 13, 09 §3 251; register 155→157 (+R-1054, R-1055, R-1056, −R-952); STATUS. - website: prowlarr-logo.svg (byte copy from felhom.eu-media) — the site gate refused every push. Not deployed — waits for the operator's yes. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
90 lines
6.0 KiB
Markdown
90 lines
6.0 KiB
Markdown
# REPORT — hub access tokens (hub v0.152.0), 2026-10-15
|
|
|
|
## For the operator — plain words
|
|
|
|
**What a token can do.** A token lets a script use the hub without your password. You manage tokens on
|
|
**Configuration → Access tokens**. You give a name, tick what it may do, and pick 30, 90, 180 or 365 days. The hub
|
|
shows the token once. It keeps only a scrambled form. Every use is logged with the token's name.
|
|
|
|
**What no token can do.** Show a box's emergency root password. Show a household's passphrase or controller key (so
|
|
the customer page and the config preview are password-only). Delete or reset anything. Change the password. Create or
|
|
revoke tokens. Reach the box API (`/api/v1`).
|
|
|
|
**Is CC using its token now?** No. Hub 0.152.0 is built and tested, but not deployed. It waits for your „yes".
|
|
|
|
**What you do now.**
|
|
1. Say „yes" — I deploy hub 0.152.0 (it includes 0.151.0).
|
|
2. On Configuration → Access tokens, create „cc-releases": *Overview: read* and *Releases: floor: write*, 90 days.
|
|
Save it on DooPlex as `~/.felhom-hub/cc-token` with `chmod 600`. Do not paste it to me.
|
|
3. I test it and tell you. Then change the hub password. CC's copy of the old password is the line `HUB_PW` in
|
|
`~/.config/credentials` on DooPlex — delete that line yourself.
|
|
4. Decide R-1055 (below).
|
|
|
|
**Was the hub ever open without a password?** The code allowed it: with no password set, the hub served the whole
|
|
operator UI with no login. The live hub was never open: today an unauthenticated request got the login page, and the
|
|
manifest carries a password hash. Fixed now: no password means every operator page answers „unavailable".
|
|
|
|
## What was built
|
|
|
|
- Token store `api_tokens` (SHA-256 only), the guard in `RequireAuth`, the route table (`hub/internal/web/routes.go`),
|
|
the Configuration card, the box API's refusal of tokens, the box API's route table (`hub/internal/api/routes.go`).
|
|
- Design: `documentation/architecture/05-hub-architecture.md` §17 (read first, with §16). Also `04` §6.2, `13` §4/§5,
|
|
`09` §3 decision 251.
|
|
- Go gate in `hub/`: `go build ./... && go vet ./... && go test ./...` → rc 0.
|
|
|
|
## Red-proofs — each guard broken on purpose, its test seen failing, then restored
|
|
|
|
Evidence: `documentation/audits/hub-tokens-2026-10-15/red-proof/` (one file per case; each shows `=== RUN` for the test).
|
|
|
|
| # | guard broken | test | failed with |
|
|
|---|---|---|---|
|
|
| 1 | operator-action entry removed from the route table | `TestRouteTable_MatchesServeHTTPSwitch` | route 21: switch has `/hosts/…/operator-action`, table has `/request-logs` |
|
|
| 2 | session-only allowed for tokens | `TestToken_SessionOnlyRoutesRefuseEveryToken` | `POST /` with an all-scope token: 200 |
|
|
| 3 | expiry not checked | `TestToken_NoTokenUnknownExpiredRevoked401` | expired token: 200, want 401 |
|
|
| 4 | group:read accepted on a write route | `TestToken_ScopeDecides` | floor:read token on floor save: 303; the floor changed |
|
|
| 5 | plain token stored | `TestAPIToken_StoredRowHasNoPlainToken` | t.db holds the plain token |
|
|
| 6 | old empty-password skip restored | `TestRequireAuth_NoPasswordRefusesEveryOperatorRoute` | no password: GET / → 200 |
|
|
| 7 | Basic passes without the header | `TestToken_CSRFRuleUnchangedForBasic` | Basic without X-Felhom-Operator: 303 |
|
|
| 8 | handler's own token check removed | `TestToken_CannotManageTokens` | handleTokenCreate under a token: 200; 2 rows |
|
|
| 9 | `GET /customers` removed from the API table | `TestAPIRouteTable_MatchesSwitch` | route 36 mismatch |
|
|
| 10 | `requireGlobalKey` returns true | `TestAPIRoutes_NoCredential` | GET /customers with no credential: 200 |
|
|
| 11 | `fhp_` check removed from the API | `TestAPI_AccessTokenIsRefusedEverywhere` | POST /report with an access token: 401 (not the 403 refusal) |
|
|
| 12 | main mounts `ServeHTTP` bare | `TestWebMountAlwaysAuthenticated` | 0 via Handler(), 1 other |
|
|
|
|
## Claims in the task that turned out wrong
|
|
|
|
- „Hungarian UI, same style as the page": the Configuration page and the whole hub UI are English (the design system:
|
|
operator surfaces are English). The card is English, like the page.
|
|
- „Mail hold — read: the held mails": the hub keeps no held mail; a hold drops it (`internal/mailhold`). Mail hold has
|
|
only a write tick (release). Releases: vouch has only a write tick too — the Day-0 manifest has no page of its own.
|
|
- „`dr-recipe.json` if it carries a secret": it carries none (`handleDRRecipeDownload`: „secret-free"); it is
|
|
`customers:read`.
|
|
- Not in the task, found: the customer page embeds the owner passphrase and the controller API key, and the config
|
|
preview carries the controller key — both session only.
|
|
|
|
## Recommendations not followed (one line each)
|
|
|
|
- „Customers write: resend claim" — followed as written, but a stolen `customers:write` token can then redirect a
|
|
household's claim code (edit e-mail, resend). Filed R-1055 for your decision; my pick is password-only.
|
|
- „Off-site read: key audit" — followed; it is a POST that runs the check now and changes no key.
|
|
|
|
## Scripts and runbooks that use the password (Part C4 — after the token works live)
|
|
|
|
| where | what | scope it needs |
|
|
|---|---|---|
|
|
| `skills/felhom-build-deploy/SKILL.md` release step 5 | global floor save | `floor:write` — the CC token has it |
|
|
| `skills/felhom-build-deploy/SKILL.md` ISO section | discard a proof install's appliance | **session only** — no token can; stays yours or stays the password until you place it |
|
|
| `~/git/misc-scripts/gitea-image-prune.sh` | reads floor, golden, agent from the Configuration page | `floor:read` — the CC token has it |
|
|
| `documentation/runbooks/RUNBOOK-manual-build.md` §prune | describes the script above | — |
|
|
|
|
Not moved yet — rule 3: until the token works live, nothing switches.
|
|
|
|
## Register
|
|
|
|
Before 155 · after 157 · opened 3 (R-1054 global API key's future, R-1055 claim-resend chain, R-1056 five routes
|
|
with no group) · closed 1 (R-952). R-1051 stays open (BLOCKED on the deploy and the token).
|
|
|
|
## Teardown
|
|
|
|
Provisioned nothing: no box, no host, no hub change. Tests ran on temp databases only.
|