hub v0.153.0: off-site and PBS-DR re-issues session only too (R-1059 = A, operator 2026-10-15); R-1059 closed
gates / gates (push) Successful in 6m41s

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 13:33:25 +02:00
parent 368ee9878d
commit 00686789e1
11 changed files with 42 additions and 22 deletions
+12 -6
View File
@@ -1,5 +1,7 @@
# REPORT — hub v0.153.0: no token sends, mints or reveals a household credential or link (2026-10-15)
`09` §3 decision 252 (operator 13:20) and R-1059 „A" (same day). Design: `05` §17 (read first).
The previous report (hub v0.152.0, access tokens, deploy and CC's switch to its token):
`git show 5d78569b:REPORT-hub-tokens.md`.
@@ -18,13 +20,14 @@ passphrase, binding an appliance. „Customers: write" keeps only block, unblock
household's e-mail and passphrase stay the same, and no customer is created. I broke the rule on purpose twice; the
test failed both times. All hub tests pass.
**Is it live?** No. Hub 0.153.0 waits for your „yes".
**Off-site (R-1059 = A, same day).** No token can re-issue a household's off-site password or PBS-DR token either.
„Off-site: write" keeps the windows and freeze.
**Is it live?** Deploying on your „yes" (see the end of this file).
**What you do now.**
1. Say „yes" to deploy hub 0.153.0.
2. Decide R-1059: a token with „Off-site: write" (yours has it) can re-issue a household's off-site password. It never
sees the password, but the backup can stop until the box picks the new one up. Keep it, or password-only?
My pick: password-only.
2. Nothing else for this; R-1059 is decided (A) and built.
## The Customers-write routes, checked one by one (`hub/internal/web/routes.go`)
@@ -37,6 +40,8 @@ test failed both times. All hub tests pass.
| `POST /customers/{id}/selfbind-link` | session only (since v0.152.0) | mints and mails a self-bind capability link |
| `POST /configs/{id}/regen-password` | session only (since v0.152.0) | mints a new owner passphrase |
| `POST /appliances/{id}/bind` | session only (since v0.152.0) | delivers the household's passphrase to the box that asks |
| `POST /configs/{id}/offsite-reissue` | session only (R-1059) | resets the household's off-site password, stages a fresh one-time secret for the box |
| `POST /configs/{id}/pbsdr-reissue` | session only (R-1059) | re-keys the household's PBS-DR token, stages a fresh one-time secret for the box |
| `POST /customers/{id}/block`, `/unblock`, `/geo/disable` | `customers:write` | no credential, link or e-mail |
The GET forms (`/configs/new`, `/configs/{id}/edit`) stay `customers:read`: they show no passphrase or key (checked in
@@ -46,13 +51,14 @@ the templates).
- Test `TestToken_CredentialAndLinkRoutesAreSessionOnly` (`hub/internal/web/tokens_test.go`).
- Red-proofs: claim-resend put back in the group → the handler was reached (502, no claim engine in the test);
edit put back → 200. Files: `documentation/audits/hub-tokens-2026-10-15/red-proof/13-claim-resend.txt`, `14-config-edit.txt`.
edit put back → 200; offsite-reissue put back → the handler was reached (502). Files:
`documentation/audits/hub-tokens-2026-10-15/red-proof/13-claim-resend.txt`, `14-config-edit.txt`, `15-offsite-reissue.txt`.
- `go build ./... && go vet ./... && go test ./...` in `hub/` → rc 0.
## Records
`09` §3 decision 252; `05` §17.2, §17.3, §17.5; hub CHANGELOG v0.153.0; STATUS.
Register: before 157 · after 157 · opened 1 (R-1059, off-site re-issue) · closed 1 (R-1055).
Register: before 157 · after 156 · opened 1 (R-1059, off-site re-issue) · closed 2 (R-1055, R-1059).
## Teardown
+1 -1
View File
@@ -44,7 +44,7 @@
## Waiting on you — do
- **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.
- **Hub 0.153.0 is ready (your ruling 13:20): no token can resend a claim code, create or edit a customer, or make a passphrase or a link.** May I deploy it? Say „yes". If not: on the live hub a token with „Customers: write" can still do those four. **Decide (R-1059):** a token with „Off-site: write" (yours has it) can re-issue a household's off-site password — it never sees it, but the backup can stop until the box picks the new one up. Keep, or password-only? **My pick: password-only.** If you do nothing: it stays.
- **Hub 0.153.0 (your rulings 13:20 + R-1059 „A"): no token can resend a claim code, create or edit a customer, make a passphrase or a link, or re-issue an off-site password.** Deploying now on your „yes".
- **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).
- **Clear demo-hp's own floor** (hub → Demo HP → floor: empty → Save floor). It still reads 0.311.0. If not: the next global raise passes demo-hp by.
@@ -553,7 +553,7 @@ Configuration, which `floor:read` opens; the mail hold is a banner, and held mai
| `vouch` | — | `POST /configuration/artifacts` (golden, agent, MinAgent) |
| `customers` | new/edit forms, DR recipe (secret-free) | block/unblock, geo disable (create, edit, create-config, claim resend: session only since v0.153.0, decision 252) |
| `hosts` | host page, log bundles, log tails | operator actions, log-bundle and log-tail requests |
| `offsite` | run the key check now (it reads, changes no key) | window grant, weekly windows on/off, off-site and PBS-DR re-issue, freeze |
| `offsite` | run the key check now (it reads, changes no key) | window grant, weekly windows on/off, freeze (the two re-issues: session only since v0.153.0, R-1059) |
| `os` | the fleet queue (`/os/fleet`) | approve (guest, Docker, Proxmox, kernel), ring, on/off |
| `mailhold` | — | release |
@@ -624,9 +624,9 @@ method (405), so a token gets 403.
| `/offsite/windows-enabled` | — | `offsite:write` | |
| `/os/{id}` | `os:read` | `os:write` | |
| `/offsite/key-audit` | — | `offsite:read` | Runs the daily key check now: it reads keys and reports, it changes none. |
| `/configs/{id}/offsite-reissue` | — | `offsite:write` | |
| `/configs/{id}/offsite-reissue` | — | **session only** | Resets the household's off-site password and stages a fresh one-time secret for the box (R-1059, ruling 2026-10-15). |
| `/configs/{id}/claim-resend` | — | **session only** | Sends a fresh claim code to the household's e-mail (R-1055, operator ruling 2026-10-15 13:20). |
| `/configs/{id}/pbsdr-reissue` | — | `offsite:write` | |
| `/configs/{id}/pbsdr-reissue` | — | **session only** | Re-keys the household's PBS-DR token and stages a fresh one-time secret for the box (R-1059, ruling 2026-10-15). |
| `/configs/{id}/offsite-freeze` | — | `offsite:write` | |
| `/configs/{id}/preview` | **session only** | — | The preview is the generated controller config — it carries the controller API key. |
| `/configs/{id}/regen-password` | — | **session only** | |
@@ -634,7 +634,7 @@ method (405), so a token gets 403.
| `/configs/{id}` | `overview:read` | `overview:read` | |
**Session only, by reason:** a household credential or link is sent, minted or its e-mail changed (claim resend,
customer create and edit, create-config — `09` §3 decision 252, `TestToken_CredentialAndLinkRoutesAreSessionOnly`); a
customer create and edit, create-config, the off-site and PBS-DR re-issues — `09` §3 decision 252, `TestToken_CredentialAndLinkRoutesAreSessionOnly`); a
secret is shown or minted (reveal recovery credential; the customer page — it embeds the
owner passphrase and the controller API key; the config preview — the generated controller config carries that key;
regen-password; the self-bind link; an appliance bind — it delivers the passphrase to the box that asks); something is
@@ -665,9 +665,8 @@ replace the global key yet (R-1054).
takeover path. Closed in v0.153.0 (operator ruling 2026-10-15 13:20, `09` §3 decision 252, R-1055): every route that
sends, mints or reveals a household credential or link, or changes the e-mail, is session only. The group keeps block,
unblock and geo disable.
- **An `offsite:write` token** can re-issue a household's off-site or PBS-DR credential (a fresh one-time secret staged
for the box). It never sees the secret; it can break a household's backup until the box picks the new one up. Open
for the operator (R-1059).
- **An `offsite:write` token** could, in v0.152.0, re-issue a household's off-site or PBS-DR credential and so stop its
backup for a while. Session only since v0.153.0 (operator 2026-10-15, R-1059); the group keeps the windows and freeze.
- **No token** can reach the break-glass password, the escrow, the off-site keys, or any destructive verb.
### 17.6 No operator password configured = closed
@@ -945,6 +945,8 @@ its length, and both fixes cost something the household would notice — operato
self-bind link), create-config from a report (mints passphrase + API key) — plus the three already session only:
the self-bind link, regen-password, appliance bind. „Customers: write" keeps block, unblock and geo disable. Pinned by
`TestToken_CredentialAndLinkRoutesAreSessionOnly` (hub v0.153.0, `05` §17.3). Amends decision 251's groups.
**Extended the same day (operator, R-1059 = A):** the off-site and PBS-DR re-issues (they mint a household
credential for the box) are session only too; „Off-site: write" keeps the windows and freeze.
### 2026-10-15 (07:48–08:20) — bake first; Plex through its own key; HSTS one day (recorded before the media build)
@@ -65,7 +65,7 @@ OS packages (exact versions, apt-signed), the installer script (by tag, R-980).
| **added (R-1007):** controller → an app's own job queue | holds — nothing new | the `after_unclean_boot` commands run inside the app's container through the docker socket the controller already holds, and read the app's database with a read-only session: no key into the household's account, nothing a compromised controller could not already do (exec in any app, read its database). The household's lost-file names stay on the box (`app.yaml`, the app page); the operator event carries counts only, and a failing command's output is never logged (security review of controller `8d862e4`, fixed in `3b85a0c`) |
| **added:** agent ↔ root on the host | holds by the sudoers | every root verb is one regex-anchored line or a root wrapper that re-checks its input (`03` §3); agent v0.156.1 adds one delete: `felhom-vzdump-partial-clean` removes only a DEAD half-written vzdump of the named guest on a `dir` storage (no vzdump on the host, nothing holding it open) — a compromised agent gains the deletion of a leftover that is not a backup |
| agent ↔ hub | holds for destructive ops | signed jobs verified in the documented order; unsigned spec has no size bounds (R-984) |
| **added 2026-10-15:** operator script ↔ hub UI | holds from hub v0.152.0 | scripts used the operator password, which can do everything; now a scoped, expiring, hashed token (`05` §17); a token never reaches a secret, a delete or `/api/v1`. No token sends, mints or reveals a household credential or link since v0.153.0 (`09` §3 decision 252, R-1055 closed); open: an `offsite:write` token can re-issue a household's off-site credential (R-1059); the global API key is still one all-powerful bearer (R-1054) |
| **added 2026-10-15:** operator script ↔ hub UI | holds from hub v0.152.0 | scripts used the operator password, which can do everything; now a scoped, expiring, hashed token (`05` §17); a token never reaches a secret, a delete or `/api/v1`. No token sends, mints or reveals a household credential or link since v0.153.0 (`09` §3 decision 252, R-1055 closed) — the off-site re-issues included (R-1059); the global API key is still one all-powerful bearer (R-1054) |
| controller ↔ hub | **did not hold until 2026-10-12** | the global key was public (R-950, rotated) and three routes needed no key (R-951, fixed); per-box keys are bound to their box (R-955) |
| box ↔ PBS | holds as written | ciphertext only; the zero-knowledge wording is stronger than a live box allows (R-985) |
| guest ↔ Proxmox host | holds | the guest has no Proxmox credentials (measured) |
@@ -0,0 +1,6 @@
=== RUN TestToken_CredentialAndLinkRoutesAreSessionOnly
tokens_test.go:582: POST /configs/c1/offsite-reissue (resets the household's off-site password) with an all-scope token: 502 "Offsite provisioning is not configured on this hub\n", want 403 session only
--- FAIL: TestToken_CredentialAndLinkRoutesAreSessionOnly (0.05s)
FAIL
FAIL gitea.dooplex.hu/admin/felhom-hub/internal/web 0.077s
FAIL
+1
View File
@@ -36,6 +36,7 @@
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1059** | **A token with „Off-site: write" could re-issue a household's off-site or PBS-DR credential and stop its backup for a while.** (P3) | CLOSED 2026-10-15 — operator „A" (session only); built in hub v0.153.0 with decision 252: `/configs/{id}/offsite-reissue` and `/pbsdr-reissue` are session only. Filed and closed the same day. | `TestToken_CredentialAndLinkRoutesAreSessionOnly` (red-proof `audits/hub-tokens-2026-10-15/red-proof/15-offsite-reissue.txt`); `05` §17.3 |
| **R-1055** | **A stolen „Customers: write" token could take over a household's dashboard: change the e-mail, then resend the claim code.** (P3) | CLOSED 2026-10-15 — operator ruling 13:20 (A, `09` §3 decision 252); **built in hub v0.153.0 (not deployed — the operator's „yes" first)**: claim resend, customer create, customer edit and create-config are session only, with the self-bind link, regen-password and appliance bind already so. Full text: `git show 5d78569b:documentation/backlog/OPEN-ITEMS.md` | `TestToken_CredentialAndLinkRoutesAreSessionOnly` (red-proved twice: `audits/hub-tokens-2026-10-15/red-proof/13-claim-resend.txt`, `14-config-edit.txt`); `05` §17.3, §17.5 |
| **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 |
+1 -2
View File
@@ -197,7 +197,7 @@ stopping line that lies.
| **R-332** | Storage & devices | P3 | **The new Hiba-from-counters path has never fired on real hardware.** v0.215.0's whole point is a verdict the product could not previously reach, and it is proven only against the committed fixture's values in unit tests (12 scenario groups, 11 of 12 red-proofs failing as required). The live validation on demo-hp proved the **negative** — three healthy disks still read Rendben across the deploy, no false alert — and the **severity wire** end to end, but no live disk has actually reached Hiba. **This is the honest gap and it must not be closed by pointing at the fixture tests**: the drive that produced the fixture is in DooPlex, which is Tier 2 and never a drill target, and the demo boxes are all-flash and healthy | **WATCHING — NEW 2026-08-14, NARROWED same day.** One item originally in this gap is now PROVEN LIVE: the **persisted state surviving a controller restart**. The v0.215.0→v0.216.0 redeploy destroyed and rebuilt the container, and the new one read back a `changed_at` written by the PREVIOUS version (`2026-08-14T07:23:14.640216851Z`, still intact at 09:31:35Z) instead of re-baselining — Scenario L on real hardware, not just the production-path unit test. **What remains unproven is the verdict itself, plus the stronger restart half: an already-ALERTED disk not re-alerting** | a real degrading disk, or an injection harness | **Closing condition:** a live disk reaching Hiba from counters, OR a deliberate injection through the REAL pipeline (agent `/disks` → controller check → hub event), not a hand-set verdict | CC |
| **R-331** | Storage & devices | P4 | **Disk health Phase 3 — growth-rate detection, and retiring the static 64.** The v0.215.0 count backstop (64 unreadable sectors → Hiba) is **a judgement from ONE drive**: the observed benign excursion peaked at 16 and cleared inside an hour, and the terminal run passed 64 at 13 Aug 11:28 and never came back. It is deliberately a backstop BEHIND the sustain rule, not the primary signal, but it is still a magic number tuned on a single sample and it will be wrong for some drive. With Phase 2's history the box can ask the question that actually matters — *is this count climbing, and how fast* — which distinguishes a drive with eight stable aging sectors from one adding forty a day, something no static threshold can do. Revisit 64 when that exists | **READY (M) — NEW 2026-08-14** | R-330 | Growth-rate rule over persisted samples; re-derive or delete the static 64 | CC |
## Security & access — 23 rows (P1 2, P2 3, P3 12, P4 6)
## Security & access — 22 rows (P1 2, P2 3, P3 11, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -223,7 +223,6 @@ stopping line that lies.
| **R-1050** | Security & access | P4 | **Raise HSTS to one year on or after 2026-11-15, if no https trouble was reported.** `09` §3 decision 249 (D2 = A, operator 2026-10-15): HSTS stays `max-age=86400` now; after a month with no https trouble it becomes one year (`31536000`); never preload, never `includeSubDomains`. The value is `HSTSMaxAgeSeconds` in `felhom-controller/controller/internal/infra/infra.go`, pinned by `TestRenderForwardedHeaders_HTTPSOnly`. „Trouble” = any report of a name that will not open, or a box answering http after the floor. | **WATCHING** — filed 2026-10-15 | the date | On or after 2026-11-15: read the register and the operator's mail for https trouble since 2026-10-15; none → a controller release with one year (test updated), bake → vouch → floor | CC |
| **R-1054** | Security & access | P3 | **The hub's operator/global API key is still one bearer that can do everything on `/api/v1`, including reading every box's break-glass root password (`GET /admin/hosts/{id}/recovery-credential`).** Hub v0.152.0 gave the web surface scoped tokens (`05` §17) but tokens are refused on `/api/v1` by design. Who uses the global key today is not listed in one place. Fix: list its consumers; move each operator use to a scoped token on the web side (or a token class on the API), and keep the global key only where a box path needs it — or retire it. | OPEN — filed 2026-10-15 by the token build | — | List the global key's consumers (pod env, scripts, runbooks) | CC |
| **R-1057** | Security & access | P3 | **A restore of a REMOVED gated app comes back without its setup gate, even when the backup was taken before the household's setup — so the app returns open, with no account.** Measured 2026-10-15 on 9202 with Prowlarr (`audits/prowlarr-2026-10-15/box/run3/restore.txt`): installed (gated, no account made), the guarded Update's per-app backup, remove keeping the backups, the household's restore button — afterwards a stranger's `/ping` through traefik answered 200, where before the remove it answered 401 (the gate). This follows the documented rule (`felhom-controller` `internal/stacks/life_records.go` `carryLifeRecords`: no prior record = no gate, „the data comes back with its admin"); the rule's assumption fails for a backup made before the setup. For a first-visitor app (Prowlarr, n8n, …) the first stranger sets the sign-in; for Prowlarr `/initialize.json` also hands out the API key (seen before the setup in the install window — NOT measured after this restore). Needs: the household to install, let a backup run before finishing the setup, remove, restore. | **OPEN** — filed 2026-10-15 by the Prowlarr walk | — | Record the gate's state in the per-app backup and re-close it on restore when it was closed then; red-proof with this walk (`tools/walk_restore.py`) | CC |
| **R-1059** | Security & access | P3 | **A token with „Off-site: write" can re-issue a household's off-site or PBS-DR credential** (`/configs/{id}/offsite-reissue`, `/pbsdr-reissue`: the box password is reset, a fresh one-time secret is staged for the box). The token holder never sees the secret, but can break the household's backup until the box picks the new one up. Decision 252 covered „Customers: write" only; the operator's own token „cc-releases" holds `offsite:write`. | WAITING-ON-OPERATOR — filed 2026-10-15 by the decision-252 build | operator decision | Decide: keep the two re-issues in Off-site write, or session only (two lines in `hub/internal/web/routes.go`) | operator |
## Box system & updates — 13 rows (P2 1, P3 9, P4 3)
+3 -1
View File
@@ -13,7 +13,9 @@ Operator ruling 2026-10-15 13:20 (`09` §3 decision 252, R-1055 = A). `documenta
- Test `TestToken_CredentialAndLinkRoutesAreSessionOnly`: a token with every scope gets 403 „session only" on all seven;
the e-mail and the passphrase are unchanged and no customer is created; block still opens (positive control).
Red-proved twice: claim-resend back in the group → 502 from the handler (reached); edit back → 200.
- Off-site re-issue also mints a credential (for the box, never shown) — outside this ruling, filed R-1059.
- **R-1059 (operator „A", same day):** `POST /configs/{id}/offsite-reissue` and `/pbsdr-reissue` are session only too —
each mints a household credential for the box (the off-site password; the PBS-DR token). „Off-site: write" keeps the
windows and freeze. Same test; red-proved (offsite-reissue back in the group → the handler was reached, 502).
- **Not deployed** — the operator's „yes" first.
## v0.152.0 — operator access tokens with scopes; an empty password no longer opens the UI; R-952 (2026-10-15)
+5 -3
View File
@@ -43,7 +43,7 @@ var scopeGroups = []scopeGroup{
{grpVouch, "Releases: vouch", "", "golden, agent, MinAgent (the vouch)", false, true},
{grpCustomers, "Customers", "config forms, DR recipe", "block/unblock, geo disable", true, true},
{grpHosts, "Hosts & logs", "host pages, log bundles, log tails", "operator actions, log requests", true, true},
{grpOffsite, "Off-site", "run the key check", "windows, re-issue, freeze", true, true},
{grpOffsite, "Off-site", "run the key check", "windows, freeze", true, true},
{grpOS, "OS updates", "the fleet queue", "approve, ring, on/off", true, true},
{grpMailHold, "Mail hold", "", "release", false, true},
}
@@ -168,10 +168,12 @@ var operatorRoutes = []routeEntry{
{Prefix: "/os/", Get: rd(grpOS), Post: wr(grpOS)},
// Runs the daily key check now: it reads keys and reports, it changes none.
{Exact: "/offsite/key-audit", Post: rd(grpOffsite)},
{Prefix: "/configs/", Suffix: "/offsite-reissue", Post: wr(grpOffsite)},
// Resets the household's off-site password and stages a fresh one-time secret for the box (R-1059, ruling 2026-10-15).
{Prefix: "/configs/", Suffix: "/offsite-reissue", Post: sessionOnly},
// Sends a fresh claim code to the household's e-mail (R-1055, operator ruling 2026-10-15 13:20).
{Prefix: "/configs/", Suffix: "/claim-resend", Post: sessionOnly},
{Prefix: "/configs/", Suffix: "/pbsdr-reissue", Post: wr(grpOffsite)},
// Re-keys the household's PBS-DR token and stages a fresh one-time secret for the box (R-1059, ruling 2026-10-15).
{Prefix: "/configs/", Suffix: "/pbsdr-reissue", Post: sessionOnly},
{Prefix: "/configs/", Suffix: "/offsite-freeze", Post: wr(grpOffsite)},
// The preview is the generated controller config — it carries the controller API key.
{Prefix: "/configs/", Suffix: "/preview", Get: sessionOnly},
+4 -1
View File
@@ -551,7 +551,7 @@ func TestRequireAuth_NoPasswordRefusesEveryOperatorRoute(t *testing.T) {
}
}
// TestToken_CredentialAndLinkRoutesAreSessionOnly — operator ruling 2026-10-15 13:20 (`09` §3 decision 252, R-1055):
// TestToken_CredentialAndLinkRoutesAreSessionOnly — operator ruling 2026-10-15 13:20 (`09` §3 decision 252, R-1055, R-1059):
// every route that sends, mints or reveals a household credential or link, or changes a household's e-mail, is session
// only. A token holding EVERY scope gets 403 on each, and nothing changes (the e-mail, the passphrase, no new config).
// The positive control: block — still customers:write — is NOT refused.
@@ -571,6 +571,9 @@ func TestToken_CredentialAndLinkRoutesAreSessionOnly(t *testing.T) {
{"/customers/c1/selfbind-link", "mints and mails the self-bind link"},
{"/configs/c1/regen-password", "mints a new owner passphrase"},
{"/appliances/1/bind", "delivers the passphrase to the box that asks"},
// R-1059 (operator 2026-10-15): the off-site and PBS-DR re-issues mint a household credential for the box.
{"/configs/c1/offsite-reissue", "resets the household's off-site password"},
{"/configs/c1/pbsdr-reissue", "re-keys the household's PBS-DR token"},
}
form := url.Values{"email": {"attacker@example.com"}, "customer_id": {"c2"}, "customer_name": {"x"}}
for _, rt := range routes {