78121aa475
gates / gates (push) Successful in 3m57s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
53 lines
4.9 KiB
Markdown
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.
|