# 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_token` verbatim into the customer's config (`hub/internal/web/configs.go:1682-1684`); the controller writes it to a 0600 `.env` for Traefik (`controller/internal/infra/infra.go:157-159`; the row's `:123` is 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 under `felhom.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 from `OPEN-ITEMS.md` in the 2026-10-03 triage (`71b8c8c6`) and never reached `CLOSED-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 a `felhom.eu` suffix check; the form re-renders with the submitted values and one sentence. - **Red test (fails on today's code):** create customer `a` with `example.hu`; creating `b` with `example.hu`, `x.example.hu` or `t1.felhom.eu` is refused and nothing is stored. **Controls:** `b` with `example2.hu` is accepted; editing `a` with its own `example.hu` is accepted (no self-conflict); `notexample.hu` is 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.