CC uses its hub token: live C3 proof (floor no-op 303 floor_set, vouch 403, session-only 403); R-1051 closed; skill floor step + prune script on the token; STATUS: change the hub password
gates / gates (push) Successful in 6m33s

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 13:10:00 +02:00
parent 2f1f6c8052
commit 5d78569bac
8 changed files with 69 additions and 32 deletions
+27 -18
View File
@@ -6,23 +6,28 @@
**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`).
**What no token can do.** Show a box's emergency root password. Show a household's passphrase or controller key.
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".
**Is CC using its token now?** Yes. Hub 0.152.0 is live (your „yes", 2026-10-15). With the token „cc-releases":
- a floor save with the same values (0.313.0, MinAgent 0.155.0) worked, and the hub database shows it;
- the vouch was refused: „missing scope vouch:write";
- the customer page and the emergency-password reveal were refused: „session only".
All three boxes reported to the new hub after the restart.
**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).
1. Change the hub password. CC no longer needs it.
2. Delete the `HUB_PW` line in `~/.config/credentials` on DooPlex. CC does not delete it.
3. Your token is wider than the plan: it can also change off-site windows, re-issue off-site keys, freeze off-site,
and approve OS updates. CC uses only the floor. Re-create it narrower if you want; CC will not widen or narrow it.
4. Decide R-1055: should „resend claim" need your password? My pick: yes.
5. Proof-install clean-up (discarding an unclaimed appliance) is password-only now. CC will ask you to press Discard.
**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".
**Side effect.** Each floor save (two today, both with the same value) mailed you „demo-hp's own floor 0.311.0
blocks the global floor". That is the old item: clear demo-hp's own floor.
**Was the hub ever open without a password?** The code allowed it. The live hub was never open (it asked for a login,
and the manifest carries a password hash). Fixed: no password now means every operator page refuses.
## What was built
@@ -68,7 +73,7 @@ Evidence: `documentation/audits/hub-tokens-2026-10-15/red-proof/` (one file per
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)
## Scripts and runbooks moved from the password to the token (Part C4 — done)
| where | what | scope it needs |
|---|---|---|
@@ -77,13 +82,17 @@ Evidence: `documentation/audits/hub-tokens-2026-10-15/red-proof/` (one file per
| `~/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.
Moved: the skill's floor step (the recipe was run verbatim: 303 `flash=floor_set`), `gitea-image-prune.sh` (misc-scripts;
its test passes, a live dry-run read 6 in-use versions through the token, deleted 0), the runbook line. The appliance
discard is password-only: the skill now says to ask you. Nothing CC runs reads `HUB_PW` any more.
## 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).
Before 155 · after 156 · opened 3 (R-1054 global API key's future, R-1055 claim-resend chain, R-1056 five routes
with no group) · closed 2 (R-952, R-1051).
## Teardown
Provisioned nothing: no box, no host, no hub change. Tests ran on temp databases only.
Machine: nothing provisioned. Host: nothing. Hub: v0.152.0 deployed (manifest `2184897`); two floor saves with the
unchanged value; no test token created by CC (the only token is the operator's). Evidence:
`documentation/audits/hub-tokens-2026-10-15/` (`deploy.txt`, `C3-token-live.txt`, `red-proof/`).
+2 -2
View File
@@ -2,7 +2,7 @@
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
**Updated 2026-10-15 (evening).** Agent 0.156.1 + its bundle on all three boxes; controller 0.313.0 is the floor (vouched golden 0.313.0) — demo-felhom and demo-hp run it, Tester 1 still runs the test build rc.4 (R-1053); hub 0.152.0 (access tokens; deployed 2026-10-15 evening). Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
**Updated 2026-10-15 (evening).** Agent 0.156.1 + its bundle on all three boxes; controller 0.313.0 is the floor (vouched golden 0.313.0) — demo-felhom and demo-hp run it, Tester 1 still runs the test build rc.4 (R-1053); hub 0.152.0 (access tokens — CC uses its own token now, not your password). Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
`documentation/backlog/STATUS-HISTORY.md`. Reports: `REPORT-night-2026-10-14.md`, `REPORT-immich-rollout-2026-10-14.md`, `REPORT-seo-felho-tarhely-2026-10-14.md`, `REPORT-immich-repair-2026-10-14.md`, `REPORT-power-cut-fixes-2026-10-14.md`, `REPORT-night-power-2026-10-13.md`, `REPORT-poweroff.md`.
## What works
@@ -41,7 +41,7 @@
| D8 | R-206 | Install DooPlex's Docker settings from git (restarts Docker), and prune the build cache weekly: yes or no? | **Yes, in a quiet window** | — | a rebuild of DooPlex still has the settings in git | — |
## Waiting on you — do
- **Hub 0.152.0 is live (access tokens; it includes the drive-back mail).** **Do, in order:** (1) Configuration → Access tokens → create „cc-releases" with *Overview: read* and *Releases: floor: write*, 90 days, and save it on DooPlex as `~/.felhom-hub/cc-token` (`chmod 600`) — do not paste it to me; (2) after I report the token works, change the hub password, and delete the `HUB_PW` line in `~/.config/credentials`. If you do nothing: I keep using your password, for the floor only.
- **Change the hub password now — CC no longer needs it.** CC uses its token „cc-releases" (tested live: the floor works; the vouch and the password-only pages are refused). Then delete the `HUB_PW` line in `~/.config/credentials` on DooPlex (CC does not delete it). Your token is wider than planned (it can also change off-site windows and approve OS updates) — revoke and re-create it narrower if you want. If you do nothing: CC still holds a password that can do everything.
- **Decide (R-1055):** a token with „Customers: write" can change a household's e-mail and resend its claim code to it. Keep that, or make „resend claim" password-only? **My pick: password-only** (one line). If you do nothing: it stays as you wrote it; nobody holds such a token now.
- **Remove the test server „plex-k6” from your Plex account** (plex.tv → Settings → Authorized Devices). The claim test passed and the server is gone from Tester 1. If not: a dead server stays listed in your Plex apps.
- System poster needs a refresh: every public name answers only over https (http → 301).
@@ -464,7 +464,7 @@ cannot add a custom header without a CORS preflight, which the hub never answers
the route switch, so it covers every route at once (38 state-changing routes + an unknown path in
`r135_csrf_test.go`); `/login` and the public `/bind/<token>` stay exempt (no operator session to ride; the bind token
is the capability). The other choice — dropping browser-usable Basic auth entirely — was not taken: CC's headless runs
and the runbooks drive the hub with Basic auth, and the header costs them one flag.
and the runbooks drive the hub with Basic auth, and the header costs them one flag. *Since v0.152.0 CC's scripts use a scoped token instead (§17); Basic stays for the operator's own scripts.*
### 16.2 Secrets at rest in `hub.db` (R-133, R-821)
@@ -0,0 +1,18 @@
1 floor no-op save: (303, '/configuration?flash=floor_set')
2 vouch attempt: (403, '', 'Forbidden: missing scope vouch:write')
3 session-only (customer page): (403, '', 'Forbidden: session only — never by a token')
3b session-only (reveal): (403, '', 'Forbidden: session only — never by a token')
4 read Configuration: 200
DB ('min_controller_version', '0.313.0')
DB ('min_controller_version_declared_min_agent', '0.313.0=0.155.0')
DB token ('cc-releases', '2026-10-15T11:07:33Z', '10.42.0.1')
2026/10/15 13:06:06 [INFO] access token "cc-releases" created from 192.168.0.110: scopes overview:read,floor:read,floor:write,customers:read,hosts:read,offsite:read,offsite:write,os:read,os:write, expires 2027-04-13T11:06:06Z
2026/10/15 13:07:32 [INFO] Global controller-version floor set to "0.313.0" (declared MinAgent "0.155.0")
2026/10/15 13:07:32 [WARN] global floor 0.313.0 does NOT move customer demo-hp: its own floor 0.311.0 wins (set 2026-10-14 15:18 UTC) — clear it on the customer page to let the global floor apply
2026/10/15 13:07:32 [INFO] Operator email sent for /floor_raise_skipped
2026/10/15 13:07:33 [INFO] access token "cc-releases": POST /configuration/global-floor → 303
2026/10/15 13:07:33 [WARN] access token "cc-releases" refused: missing scope vouch:write — POST /configuration/artifacts → 403
2026/10/15 13:07:33 [WARN] access token "cc-releases" refused: session only — never by a token — GET /customers/demo-felhom → 403
2026/10/15 13:07:33 [WARN] access token "cc-releases" refused: session only — never by a token — POST /hosts/demo-hp-bb76ea/reveal-recovery-credential → 403
2026/10/15 13:07:34 [INFO] access token "cc-releases": GET /configuration → 200
skill recipe run verbatim 2026-10-15 ~13:20 Budapest (no-op 0.313.0/0.155.0): HTTP/1.1 303, Location: /configuration?flash=floor_set; header file removed
+1
View File
@@ -30,6 +30,7 @@
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1051** | **CC could not raise the global floor without the operator's own password.** (P3) | CLOSED 2026-10-15 — hub v0.152.0 deployed (operator „yes"); the operator created the token „cc-releases" (2026-10-15 13:06, wider than the task's two scopes: also customers/hosts read, offsite and os read+write — the operator's choice). Live with the token: a no-op floor save 0.313.0/MinAgent 0.155.0 → 303 `flash=floor_set`, read back from the hub DB with its WAL; the vouch → 403 `missing scope vouch:write`; the customer page and a recovery-credential reveal → 403 `session only`. The `felhom-build-deploy` skill's floor step and `gitea-image-prune.sh` now use the token. Full text: `git show e92501f5:documentation/backlog/OPEN-ITEMS.md` | `audits/hub-tokens-2026-10-15/C3-token-live.txt`, `deploy.txt`; `05` §17; `09` §3 decision 251 |
| **R-952** | **Nothing failed when a new hub route was added without deciding who may call it.** (P3) | CLOSED 2026-10-15 — **built in hub v0.152.0**, both surfaces: `/api/v1` — `internal/api/routes.go` names each route's caller (public with a reason, customer, host, global, passphrase, appliance, recovery), pinned to the switch from source; every non-public route with no credential, for a household and host that exist, answers 401/403. Web — `internal/web/routes.go` classifies every route for access tokens, pinned the same way. Full text: `git show e92501f5:documentation/backlog/OPEN-ITEMS.md` | `TestAPIRouteTable_MatchesSwitch`, `TestAPIRoutes_NoCredential`, `TestRouteTable_MatchesServeHTTPSwitch` (each red-proved, `REPORT-hub-tokens.md`); `05` §17.3–17.4 |
## 2026-10-15 (afternoon) — the floor reached 0.313.0
+1 -2
View File
@@ -260,7 +260,7 @@ stopping line that lies.
| **R-1049** | Monitoring & notifications | P3 | **A pulled drive mails the operator „unexpectedly disconnected” (error), and nothing ever mails that it came back.** MEASURED 2026-10-14 night (`audits/media-drive-events-2026-10-14/K8-hub-mails.txt`, the hub DB with its WAL from the running pod, and the mailbox as the second channel): two pulls → two `storage_disconnected` mails; the hub RECEIVED both `storage_reconnected` and both `health_recovered` (severity info) and logged no notification for any of them — they are not in `recoveredPairedDownTypes` (`hub/internal/notify/dispatcher.go`), so the severity gate drops them. The R-97a shape: told it broke, never told it healed — the operator cannot tell a returned drive from one still gone without opening the hub. **BUILT 2026-10-15 — hub v0.151.0 (`12552aca`): both pairs in `recoveredPairedDownTypes`, cooldowns ended by the next loss (`TestR1049_DriveBackMailsBothEdges`, red-proved). Not deployed: the operator's „yes” first (decision 162); the image build waits for it too.** | **WAITING-ON-OPERATOR** — hub v0.151.0 code pushed, not built, not deployed | the operator's „yes” to the hub deploy | On the „yes”: build + deploy hub v0.151.0; read a „came back” mail on the next pull | CC |
## Hub & operator — 13 rows (P3 7, P4 6)
## Hub & operator — 12 rows (P3 6, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -275,7 +275,6 @@ stopping line that lies.
| **R-844** | Hub & operator | P4 | **The household's OS-update line exists only on the hub's customer timeline.** 2026-10-04: the box itself has no event surface for agent results (the controller UI shows no timeline), so `os_update_applied` is a hub customer event (info: recorded, never mailed). Its stored text is the hub's English sentence; the hu/en bundle text (`mail.event.os_update_applied`) is used only if it is ever mailed. Fix direction: a controller-side line (the controller already polls the agent's local API) when the box gets a household timeline. `audits/os-guest-lane-2026-10-04/partG/hub-customer-timeline-demo-hp.txt` | **READY — owner: CC** **2026-10-05 (burn-down night): NEEDS A DESIGN** — a household timeline on the box does not exist yet. | — | — | CC |
| **R-1002** | Hub & operator | P4 | **A box the household switched off reads ONLINE on the hub's hosts page for its first 45 minutes** (seen live 2026-10-13, R-998 proof, `audits/poweroff-2026-10-13/B-hub-watch.log`: the „Power: Kikapcsolva…" line at 18:53, the KIKAPCSOLVA badge only at 19:38, when the host report turned stale). The customer page read „warn" in part of that gap (seen, not captured). No alarm and no mail fired; the operator page is only slow to say it. | **READY** — filed 2026-10-13 | — | CC: let `hostDisplayStatus` read `deliberate_off_at` before staleness, with a render test per state; next hub release | CC |
| **R-1011** | Hub & operator | P3 | **The Day-0 „Agent" list offers step-bundle packages (`0.156.1-step1`, `0.152.0-step1` …) as agent versions; one has no agent binary, so the hub shows the bundle JSON's sha (`FileSHA256` falls back to the first file) and a vouch would point fresh installs at a package with no binary.** Seen 2026-10-14 (`/configuration`: two „0.156.1" entries, `ac83ea41…` is the step bundle). | **READY** — filed 2026-10-14 | — | CC: list only versions whose package holds the `felhom-agent` file (refuse a missing preferred file instead of falling back), with a test; next hub release | CC |
| **R-1051** | Hub & operator | P3 | **CC cannot raise the global floor without the operator's own password.** `09` §3 decision 250 lets CC raise the floor after the three test boxes, but the only route was the operator page with the operator password. Operator ruling 2026-10-15 11:38 (`09` §3 decision 251): scoped tokens; until CC's token works, the password for the floor only. | **BLOCKED** — hub v0.152.0 DEPLOYED 2026-10-15 (`05` §17: tokens with groups, CC = `overview:read` + `floor:write`) | the operator creates „cc-releases" and saves it to `~/.felhom-hub/cc-token` | A no-op floor save with the token, read back; a vouch attempt → 403; a session-only route → 403; then close | operator |
| **R-1056** | Hub & operator | P4 | **Five operator routes fit no token group, so they are session only until the operator places them:** apps reset-telemetry and dismiss-issues, the Configuration asset re-seed, the off-site WireGuard endpoint save, the off-site abandon-cancel (`05` §17.3). Nothing needs them by token today. | WAITING-ON-OPERATOR — filed 2026-10-15 by the token build | operator decision | Place each in a group, or leave session only | operator |
@@ -246,7 +246,7 @@ The full 0.188.0 run, with the observables: `documentation/audits/tester-gate-go
When the Gitea volume fills, `~/git/misc-scripts/gitea-image-prune.sh` (repo `admin/misc-scripts`) prunes old versions.
**Dry-run first, always** (`./gitea-image-prune.sh --all prune`, then `--type generic --all prune`): it keeps the newest
20 of each package plus every version in use — the floor, the vouched golden, the vouched agent and `min_agent` (read
from the hub with `HUB_PW`), the images the vouched golden baked (its `bake.log` here), the hub `manifests/hub.yaml`
from the hub with the scoped token `HUB_TOKEN`, `floor:read`), the images the vouched golden baked (its `bake.log` here), the hub `manifests/hub.yaml`
runs — and prints each with its reason. It refuses when that list cannot be read. **`--apply` is a person's act**,
after reading the dry-run; nothing schedules it. The August 2026 `--keep 7` run (HM-024) predates the rule.
+18 -8
View File
@@ -61,10 +61,20 @@ Check current live version first: same `docker ps` command, or `cat /etc/felhom-
3. **The three test boxes through the normal update path:** a per-customer floor for Tester 1, demo-hp and demo-felhom at
the release, MinAgent declared; read back each box's `docker ps` and its `SetFloor`/self-update log lines.
4. **The box walk green on all three** — `python3 scripts/https_only_check.py` included (felhom.eu).
5. **Raise the global floor**, MinAgent declared (from the CHANGELOG header): hub → Configuration → Managed updates →
floor + `min_agent`, Save; read back from the hub DB with its WAL (`hub_settings.min_controller_version` and
`…_declared_min_agent`). **Route (R-1051):** the only way today is the operator page with the operator's own password;
until the operator grants a scoped route, this save is the operator's — CC asks for it with the exact values.
5. **Raise the global floor**, MinAgent declared (from the CHANGELOG header), with **CC's hub token** — never the
operator password (`09` §3 decision 251; hub ≥ 0.152.0, `05` §17). The token is `HUB_TOKEN` in
`~/.config/credentials` (read it with `read_credential.py`; never `source` that file), and goes to curl through a
0600 header file, never on the command line:
```bash
T=$(mktemp); python3 $FELHOM_ROOT/git/felhom.eu/scripts/read_credential.py HUB_TOKEN "$T" >/dev/null \
&& { printf 'Authorization: Bearer %s\n' "$(cat "$T")" > "$T.h"; chmod 600 "$T.h"; }; rm -f "$T"
curl -s -o /dev/null -D - -H @"$T.h" --data-urlencode min_controller_version=<VER> --data-urlencode min_agent=<MA> \
http://<hub-clusterIP>:8080/configuration/global-floor | grep -iE '^HTTP|^location'; rm -f "$T.h"
```
**Success is `303` with `Location: /configuration?flash=floor_set`** — a 303 to any other flash is a REFUSAL
(`floor_needs_min_agent`, `floor_invalid`, …). `401` = token expired/revoked; `403` names the missing scope. Then read
back from the hub DB **with its WAL** (`hub_settings.min_controller_version` and `…_declared_min_agent`). The vouch
(`/configuration/artifacts`) is NOT CC's — the token cannot do it (403), and must not be widened to.
6. Not at night unless the night brief says so. **Agent releases still stop for the operator's vouch**; never set a floor
whose MinAgent is above the vouched agent. The golden is baked weekly (Sunday, `nightly-rotation.md`); CC never vouches.
@@ -157,10 +167,10 @@ curl -fsSL -o /tmp/rt.iso https://iso.felhom.eu/felhom-installer-<VER>-pve<PVE>.
Judge completion from `qm config` + disk usage, never from the screen.
- **Verify focus by screendump before every `Enter`.** TUI: red-highlighted button, tab order. GTK:
dashed focus ring, and `Enter` lands in text *fields*, not `Next`. Not checking once aborted an install.
- **Proof installs register unclaimed appliances at the hub — discard them** or R-131 grows:
`curl -u ":$HUB_PW" -H "X-Felhom-Operator: cli" -X POST http://<hub-clusterIP>:8080/appliances/<id>/discard` → 303.
**Every Basic-auth POST to the hub needs `-H "X-Felhom-Operator: cli"` since hub v0.135.0 (R-135)** — without it the
CSRF gate answers 403 (a browser on another site cannot add that header; that is the protection).
- **Proof installs register unclaimed appliances at the hub — discard them** or R-131 grows. **The discard is session
only** (hub ≥ 0.152.0, `05` §17.3 — no token can do it, and CC holds no hub password any more): ask the operator to
press Discard on the hub's Hosts page, naming the appliance id. (A Basic-auth POST — the operator's own script —
still needs `-H "X-Felhom-Operator: cli"`, R-135.)
> **Never pair `-w '%{redirect_url}'` with `-u` or `--netrc` (R-580).** curl rebuilds the request URL
> for that variable **with the credentials in it**, so the hub password is printed even though it