- 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
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.
- Say „yes" — I deploy hub 0.152.0 (it includes 0.151.0).
- On Configuration → Access tokens, create „cc-releases": Overview: read and Releases: floor: write, 90 days.
Save it on DooPlex as
~/.felhom-hub/cc-tokenwithchmod 600. Do not paste it to me. - I test it and tell you. Then change the hub password. CC's copy of the old password is the line
HUB_PWin~/.config/credentialson DooPlex — delete that line yourself. - 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 inRequireAuth, 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). Also04§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.jsonif it carries a secret": it carries none (handleDRRecipeDownload: „secret-free"); it iscustomers: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:writetoken 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.