Files
felhom.eu/REPORT-hub-tokens.md
T
admin d62cc08b2e
gates / gates (push) Successful in 6m44s
hub v0.152.0: operator access tokens with scopes (09 §3 decision 251, operator 2026-10-15 11:38)
- 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
2026-10-15 12:20:35 +02:00

6.0 KiB

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.