diff --git a/REPORT-hub-tokens.md b/REPORT-hub-tokens.md index b6dae07f..a4ad7e0f 100644 --- a/REPORT-hub-tokens.md +++ b/REPORT-hub-tokens.md @@ -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/`). diff --git a/STATUS.md b/STATUS.md index 6ca039c9..5148f41a 100644 --- a/STATUS.md +++ b/STATUS.md @@ -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). diff --git a/documentation/architecture/05-hub-architecture.md b/documentation/architecture/05-hub-architecture.md index c1081e4b..034971e7 100644 --- a/documentation/architecture/05-hub-architecture.md +++ b/documentation/architecture/05-hub-architecture.md @@ -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/` 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) diff --git a/documentation/audits/hub-tokens-2026-10-15/C3-token-live.txt b/documentation/audits/hub-tokens-2026-10-15/C3-token-live.txt new file mode 100644 index 00000000..6a08885b --- /dev/null +++ b/documentation/audits/hub-tokens-2026-10-15/C3-token-live.txt @@ -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 diff --git a/documentation/backlog/CLOSED-ITEMS.md b/documentation/backlog/CLOSED-ITEMS.md index 90c77f91..81a87839 100644 --- a/documentation/backlog/CLOSED-ITEMS.md +++ b/documentation/backlog/CLOSED-ITEMS.md @@ -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 diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index ec684001..630bdfba 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -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 | diff --git a/documentation/runbooks/RUNBOOK-manual-build.md b/documentation/runbooks/RUNBOOK-manual-build.md index 043ead5d..9124e8b2 100644 --- a/documentation/runbooks/RUNBOOK-manual-build.md +++ b/documentation/runbooks/RUNBOOK-manual-build.md @@ -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. diff --git a/skills/felhom-build-deploy/SKILL.md b/skills/felhom-build-deploy/SKILL.md index 62a8f705..95f51e05 100644 --- a/skills/felhom-build-deploy/SKILL.md +++ b/skills/felhom-build-deploy/SKILL.md @@ -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= --data-urlencode min_agent= \ + http://: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--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://:8080/appliances//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