The Fat Cat board-router service token does not exist — the audit item was inherited from a proposal, not from production
The question
Is the Fat Cat
board-routerservice token scoped per-app or zone-wide? — quick check; if zone-wide, real find.
Follow-up #3 from the 2026-05-11 Cloudflare tunnel security brief for the Mac mini, which flagged the token's scope as an open one-time founder check.
What we already know (from the vault)
- [[../../08-tooling/2026-05-01-fat-cat-council-architecture-proposal.md]] — the origin of the claim. Its Cloudflare Access bullet reads: "Already in production for hq.raydata.co. Gates the
board-routerendpoint with a service token (header pairCF-Access-Client-Id/CF-Access-Client-Secret)." But the doc's own frontmatter isstatus: proposal, and its roadmap puts Access +board-routerin Phase 2 ("Add R2 + Cloudflare Access (1 evening)… Add theboard-routerWorker as a thin proxy"). The "already in production" clause attaches to Access itself, not toboard-router. Open question #5 in the same doc is still asking "Service-token vs OAuth for Ray's invocation." - [[2026-05-11-cloudflare-tunnel-security-mac-mini.md]] — the parent brief read that bullet as live config: "The Fat Cat Council
board-routerendpoint already uses a service token… make sure that token is scoped to only theboard-routerapp/path, not the entirehq.raydata.cozone." It then filed the scope check as live discipline item #5. - [[../../01-projects/hq-raydata-co/2026-04-22-hq-proposal-v1.md]] — the real Access deployment: Google SSO restricted to
ben@raydata.co, no service-token path in the v1 design. - [[../../08-tooling/2026-06-04-cloudflare-self-managed-oauth-evaluation.md]] — filed, not adopted; confirms the account's non-human-identity surface stayed deliberately small.
What the web says
- Service tokens are account-level credentials, not application-bound objects. They are created and listed at
/accounts/$ACCOUNT_ID/access/service_tokens(Cloudflare One — Service tokens); the dashboard path is Zero Trust → Access controls → Service credentials → Service Tokens, with no per-application scoping at creation. - A token's reach is determined entirely by per-application policy, not by the token. An app must carry a policy that accepts the token, and per the docs you "make sure to set the policy action to Service Auth; otherwise, Access will prompt for an identity provider login."
- The resulting credential is app-scoped at redemption: "If the service token is valid, Access generates a JWT scoped to the application in the form of a CF_Authorization cookie."
- The load-bearing distinction is the policy selector, and there are two (Cloudflare One — Access policies):
- Service Token — "the request will need to present the correct service token headers configured for the specific application." Binds one named
token_idto that app. - Any Access Service Token — "the request will need to present the headers for any service token created for this account." This is the real over-broad footgun — any token in the account opens the app.
- Service Token — "the request will need to present the correct service token headers configured for the specific application." Binds one named
- Both selectors are classed non-identity-based, meaning they deliberately bypass IdP login. That is the mechanism by which a service-token policy can sidestep Google SSO on whatever app carries it.
- Cloudflare shipped fine-grained permissions for Access policies and service tokens (2026-02-13), adding resource-scoped admin roles for individual tokens — an administrative RBAC scope, distinct from the runtime policy scope above. Don't conflate the two.
- Cloudflare's own permissions best-practices note account-level scopes "apply to all zones, and all other resources within the account."
Convergences and contradictions
- Contradiction (the finding): the question's premise is false. The vault says
board-routeris gated by a service token. The live account has noboard-routerservice token, noboard-routerAccess app, and nocouncil.raydata.coDNS record. The Fat Cat Council was never built past proposal. - Convergence on the architecture: docs and the live config agree that scope lives in the policy, not the token — and the one real service-token policy in the account uses the narrow specific-token selector, matching Cloudflare's own best practice.
- Category error to retire: "zone-wide" isn't a mode a service token can have. Access applications are account-level objects with hostname destinations; a zone is not the unit of Access scoping. The thing the parent brief was groping toward ("token bypasses SSO across the whole HQ") has a real name —
any_valid_service_token— and it is not in use here.
Synthesis for RDCO
Half (a) — VERIFIED against primary docs read in this run. A Cloudflare Access service token is an account-level credential whose blast radius is the set of applications whose policies accept it. It is neither inherently per-app nor zone-wide. It becomes per-app when an application's policy includes a specific service_token / token_id. It becomes effectively account-wide-by-app-count when a policy uses Any Access Service Token, which admits every token in the account. The correct audit question for any RDCO Access app is therefore never "how is the token scoped?" but "which policies accept a service token, and do they name a specific token_id or any valid one?"
Half (b) — VERIFIED, and the answer is that the subject doesn't exist. Read-only GETs via ~/.claude/scripts/cloudflare-api.sh against account 5d4a0efe3ae671cf9cf3265eb1ff738c returned, literally:
GET /accounts/.../access/service_tokens→ exactly one token,total_count: 1:name: "hq-frontend-to-api",id: 6b0fa8f4-dd8a-41c6-ab4f-87acf7cef600,client_id: d409f137c2c6036fa5274abffbff7c88.access, created2026-04-23,last_seen_at: 2026-07-14T13:22:39Z. There is noboard-routertoken.GET /accounts/.../access/apps→ 5 apps:quick-sites: home(home.rdco.dev),quick-sites: demo(demo.rdco.dev),quick-sites: all (private by default)(*.rdco.dev),HQ Ray Data API(hq-api.raydata.co),HQ Ray Data(hq.raydata.co). Noboard-router, no council app. Raw-JSON grep forboard-router→ 0; forcouncil→ 0.- Exactly one policy in the entire account accepts a service token: on
HQ Ray Data API(hq-api.raydata.co), namedAllow hq-frontend service token,decision: non_identity,precedence: 2, include =[{"service_token":{"token_id":"6b0fa8f4-dd8a-41c6-ab4f-87acf7cef600"}}]. That is the specific-token selector naming one token id. Raw-JSON grep forany_valid_service_token→ 0 across all 5 apps. - DNS across all 4 zones (raydata.co, rdco.dev, squarelypuzzle.com, squarelypuzzles.com) shows no
councilorboardhostname; onlyhq-api.raydata.co → …cfargotunnel.comandhq.raydata.co → hq-raydata-co.pages.dev.
So: no alarm, but a real find of a different shape. The anticipated bad outcome ("if zone-wide, real find") did not occur, and I am not going to manufacture it. The nearest real analog — hq-frontend-to-api, the only non-human identity in the account — is correctly scoped per-app by specific token_id, on the API app only. Critically, the SSO-bypassing non_identity policy exists only on hq-api.raydata.co; the human-facing hq.raydata.co carries only Allow Ben only (email). The token cannot bypass Google SSO on the HQ frontend. The *.rdco.dev wildcard app is email-gated with no service-token path. This is the configuration the parent brief was asking us to confirm, and it holds — for a token that isn't the one it asked about.
The actual finding is a vault-integrity one, and it is the reusable lesson. A status: proposal document's future-tense architecture ("Gates the board-router endpoint with a service token") was read by a downstream deep-research brief as a statement of live production fact, promoted into a "live discipline TODAY" checklist, and left open as a founder action item for two months against infrastructure that was never built. Nothing was insecure; attention was spent auditing a phantom. This is the tainted-intermediate propagation pattern already in memory (feedback_workflow_agent_output_integrity) showing up across documents rather than across sub-agents: the parent brief's "verify the service-token policy is scoped to the board-router app only" gap-to-close row reads as authoritative and cites a real vault doc, but no gate in the chain checked whether the subject existed. The cheap durable fix is a rule: when a brief cites a proposal, it inherits the proposal's status — architecture described in a status: proposal doc is a plan, not config, and any security item derived from it must be checked against live state before it becomes a checklist row.
Two low-priority hygiene notes fell out of the live read, neither urgent, neither what was asked: hq-frontend-to-api has duration: "forever" (expires_at: 2126-03-30) and client_secret_version: 1 — created 2026-04-23, never rotated, ~3 months old, actively in use (last seen 2026-07-14). That is consistent with the parent brief's "quarterly rotation as discipline" line and is the one genuine, if minor, open thread.
Why this is in the vault
Closes follow-up #3 and retires item #5 from the [[2026-05-11-cloudflare-tunnel-security-mac-mini.md]] "live discipline TODAY" checklist — a founder action item that has been open since May against infrastructure that does not exist. It also supplies the reusable audit question for RDCO's Access surface (any_valid_service_token vs specific token_id) that the Fat Cat Council would need at Phase 2 if it is ever built, and documents the proposal-read-as-production failure mode for the /deep-research engine.
Open follow-ups
- Should
hq-frontend-to-api(duration: forever, never rotated, 3 months old) get a rotation + a finite expiry, or is "forever + single-user + per-app scoped" acceptable for RDCO's threat model? Leans acceptable; rotation is cheap. - Should the /deep-research and /curiosity skills carry an explicit "a cited
status: proposaldoc describes a plan, not live config — verify against live state before filing a security checklist item" gate? - Is the Fat Cat Council still a live intention at all, or should [[../../08-tooling/2026-05-01-fat-cat-council-architecture-proposal.md]] be marked
status: dormantso it stops seeding phantom audit items downstream? - Worth a standing quarterly one-liner that greps all Access policies for
any_valid_service_tokenand reports non-zero? Cheap, and it is the only genuinely dangerous selector. - Follow-ups #1 (FileVault on?) and #2 (live hq.raydata.co policy literal-email?) from the parent brief remain open — note #2 is now partially answered here:
HQ Ray DatashowsAllow Ben onlywith a literalemail: ben@raydata.coinclude, which appears to satisfy it. - Do the three
quick-sitesapps' shared policy (ben@raydata.co,bwilson668@gmail.com,ray@raydata.co) intentionally include the personal Gmail, or is that drift from the literal-email discipline the parent brief argued for?
Related
- [[2026-05-11-cloudflare-tunnel-security-mac-mini.md]] — the parent brief; this closes its follow-up #3 and retires its live-discipline item #5
- [[../../08-tooling/2026-05-01-fat-cat-council-architecture-proposal.md]] — the
status: proposalsource of theboard-routerclaim - [[../../01-projects/hq-raydata-co/2026-04-22-hq-proposal-v1.md]] — the Access deployment that actually exists
- [[../../08-tooling/2026-06-04-cloudflare-self-managed-oauth-evaluation.md]] — adjacent non-human-identity evaluation, filed not adopted
- [[../../02-sops/2026-04-26-rdco-web-app-deployment-standard.md]] — Cloudflare token rotation discipline this brief's hygiene note leans on
Sources
Vault
~/rdco-vault/06-reference/research/2026-05-11-cloudflare-tunnel-security-mac-mini.md~/rdco-vault/08-tooling/2026-05-01-fat-cat-council-architecture-proposal.md~/rdco-vault/01-projects/hq-raydata-co/2026-04-22-hq-proposal-v1.md~/rdco-vault/08-tooling/2026-06-04-cloudflare-self-managed-oauth-evaluation.md~/rdco-vault/02-sops/2026-04-26-rdco-web-app-deployment-standard.md~/.claude/skills/cloudflare/SKILL.md— wrapper usage + token scope
Live config (read-only GETs, 2026-07-16, via ~/.claude/scripts/cloudflare-api.sh)
GET /accounts/5d4a0efe3ae671cf9cf3265eb1ff738c(whoami)GET /accounts/5d4a0efe3ae671cf9cf3265eb1ff738c/access/service_tokensGET /accounts/5d4a0efe3ae671cf9cf3265eb1ff738c/access/appsGET /zones+GET /zones/<id>/dns_recordsacross all 4 zones
Web
- Service tokens — Cloudflare One docs
- Access policies — Cloudflare One docs
- Fine-grained permissions for Access policies and service tokens — Cloudflare Changelog, 2026-02-13
- Cloudflare account permissions, how to use them, and best practices — Cloudflare blog
- Role scopes — Cloudflare Fundamentals docs
Changelog
- 2026-07-16 — initial brief filed by /deep-research engine; closes follow-up #3 of the 2026-05-11 parent brief.