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

53 lines
4.9 KiB
Markdown

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