Files
felhom.eu/documentation/audits/day-2026-10-08/design-R-138.md
T

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_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.