Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
4.9 KiB
R-138 — a Cloudflare token that can write a shared zone, on a customer's box: a one-page design (2026-10-08)
Baselines read: felhom.eu b2dce901, felhom-controller a0370b4. Architecture: 01-topology-and-trust.md §5
(trust boundaries: „hub ↔ Cloudflare API") and §7 (networking: „Every customer has their OWN domain — never a name
under felhom.eu", operator ruling 2026-09-14). Status: design only, nothing built. The token itself is stored
out-of-band; it was not read, printed or used for this page.
1. The problem, and what changed under it
- The mechanism is still as the row says. The hub form copies
cf_api_tokenverbatim into the customer's config (hub/internal/web/configs.go:1682-1684); the controller writes it to a 0600.envfor Traefik (controller/internal/infra/infra.go:157-159; the row's:123is stale). An empty token selects HTTP-01 (controller/internal/infra/templates/traefik.yml.tmpl:52-61). The controller also uses it for the geo-WAF (controller/internal/web/handlers.go:642). - The row's risk needs a SHARED customer zone, and the design has ruled that out.
01§7 (2026-09-14): every customer has their own domain, never a name underfelhom.eu. Today's boxes match: demo-felhom, demo-hp and Tester 1 (enkicsifelhom.hu,operations/nodes.md:44) each sit on their own zone. The shared-zone plan the row was written against (audits/RECON-subdomain-onboarding-2026-07-31.md§5) is not the plan any more. - But nothing ENFORCES the ruling. The hub accepts any domain, including one equal to or under another customer's
domain, or under
felhom.eu:domain TEXT NOT NULL DEFAULT ''with no uniqueness (hub/internal/store/store.go:164), and the create path refuses only a duplicate customer id (configs.go:740). That check was row R-415 (READY, XS) — it was removed fromOPEN-ITEMS.mdin the 2026-10-03 triage (71b8c8c6) and never reachedCLOSED-ITEMS.md; it is lost, not closed. - A second residue the row does not name: the four zones sit in ONE Cloudflare account (RECON §2, line 163). A token minted with account scope (all zones) instead of one zone would let one box rewrite every household's DNS — the same blast radius as a shared zone, by a typing mistake in the token wizard. Nobody checks the token's reach.
2. Options
| What | Costs | Risk | |
|---|---|---|---|
| A | Close R-138 on the ruling; do nothing more. | Nothing. | The ruling is a sentence; an operator mistake (a duplicate or nested domain, an account-wide token) passes silently. |
| B | Enforce the ruling on save (revive R-415): the hub refuses a domain that equals, contains or sits under another customer's domain, or sits under felhom.eu. |
Hub only; one store query, one form error, tests. ~¼ session. | None to customers: it refuses an operator input that the ruling already forbids. |
| C | B plus a reach check on the token: when a cf_api_token is saved, the hub asks Cloudflare which zones that token can see (GET /zones, the call the hub already makes in hub/internal/cloudflare/unblock.go:115) and refuses unless it sees exactly the customer's own zone. |
Hub only; one outbound call at save time (an existing dependency, not a new one); a save fails while Cloudflare is down — the form must say so and keep what was typed. ~½ session. | A save blocked by a Cloudflare outage; the operator retries. |
3. The pick — B now (follows the ruling, no decision needed); C on the operator's word
B is the guard the row asked for, re-scoped: not „refuse a token for a shared-zone customer" (no such customer may exist) but „refuse the layout that would make one". C closes the account-wide-token hole, which is the same danger by another road; it adds a network call to a form save, so it is the operator's choice.
4. First slice and its red test
- Hub (B): in the create and edit paths, before any provisioning:
store.DomainConflicts(customerID, domain)and afelhom.eusuffix check; the form re-renders with the submitted values and one sentence. - Red test (fails on today's code): create customer
awithexample.hu; creatingbwithexample.hu,x.example.huort1.felhom.euis refused and nothing is stored. Controls:bwithexample2.huis accepted; editingawith its ownexample.huis accepted (no self-conflict);notexample.huis accepted (suffix match is on a label boundary). - Register: R-415 re-filed (or folded into this row) and R-138 closed on B's commit.
5. One question for the operator
When you paste a customer's Cloudflare key into the hub, should the hub check with Cloudflare that the key reaches only that customer's own domain, and refuse it otherwise? My pick: yes (option C). If you do nothing: a key that was made for the whole Cloudflare account by mistake goes onto the customer's box, and that box could change the web addresses of every other household.