{"slug":"cloudflare-os-access","title":"Cloudflare Access authenticates the edge, not your application","body":"Cloudflare Access sits between a request and an origin. For a person, it turns an application URL into an identity check: Cloudflare redirects the browser to an identity provider, applies an Access policy, and issues a signed session token. For a machine, there is no login page. It must send a service credential on the first request, and the Access policy must explicitly accept that credential.\n\nThe distinction that decides the design:\n\n> Access proves that a request satisfied an edge policy. Your application still decides what that authenticated principal may do.\n\nA service token can pass Access and still carry no human identity. A Bypass rule can make a path reachable while removing Access authentication and Access logging from that path. Deleting the application does not prove the service token was deleted, and deleting the token does not prove the application or policy disappeared. Those are separate objects with separate list and delete operations.\n\n## Evidence status\n\n**Observed** marks first-party measurements or runtime receipts from the named environment.\n**Derived** marks arithmetic calculated from cited inputs. **Specified** marks vendor or standards\ndocumentation. **Implemented** and **deployed** name code and live-state evidence, respectively.\n**Reproduced** means the stated procedure was rerun. **Externally attested** marks operator reports;\nthose reports show that an experience occurred, not that it is universal.\n\n## The request path, without product names hiding the mechanics\n\n| Stage | Human request | Machine request |\n| --- | --- | --- |\n| 1. Request arrives | Browser requests the protected hostname and path | HTTP client requests the same URL |\n| 2. Access checks credential | Looks for a valid `CF_Authorization` cookie | Looks for service-token headers or another non-human credential |\n| 3. No valid credential | Redirects to the Access login flow | Usually a `302` the client cannot use, or `401`/`403` when Service Auth handling is configured |\n| 4. Policy evaluation | Allow, Block, Bypass, or a more specific rule | Service Auth, mTLS, or Bypass |\n| 5. Origin request | Cloudflare forwards `Cf-Access-Jwt-Assertion` | Cloudflare forwards an application JWT after service authentication |\n| 6. Origin authorization | Verify signature, issuer and audience; map `email` or `sub` to an app role | Verify the same fields; map `common_name` to a synthetic machine principal |\n\nAccess is not an origin firewall. Unless the origin is connected only through Cloudflare Tunnel or otherwise restricted to Cloudflare, an attacker may try to reach it directly and avoid the Access layer. Even when every request must pass Cloudflare, the origin still verifies the JWT. Cloudflare's application-token reference is blunt: validation of the header alone is insufficient because an unverified header can be spoofed.\n\n## Create one self-hosted application in the dashboard\n\nPrerequisites: a Cloudflare account, a Zero Trust organization, a domain on Cloudflare, and an identity provider. The built-in one-time PIN flow is enough for a small first deployment; an organization using group rules should connect its existing SAML or OIDC provider and confirm the exact group claim before writing policy.\n\nCurrent dashboard path:\n\n1. Open **Zero Trust**.\n2. Go to **Access controls** → **Applications**.\n3. Select **Add an application**.\n4. Choose **Self-hosted**.\n5. Set **Application name**.\n6. Under **Session Duration**, choose how long the application JWT remains valid.\n7. Under **Add public hostname**, enter **Subdomain**, **Domain**, and optional **Path**. A path makes the Access application narrower than the hostname.\n8. Under **Access policies**, create or attach a policy.\n9. Choose the identity providers shown on the login page.\n10. Save, then test one allowed identity and one denied identity before widening the selectors.\n\nAccess applications are deny-by-default. Creating the hostname without an Allow or Service Auth policy does not grant anyone access.\n\nThe four policy actions do different jobs:\n\n| Action | What a match means | Correct use | Dangerous misunderstanding |\n| --- | --- | --- | --- |\n| **Allow** | The request may continue after identity authentication | People selected by email, IdP group, country, device posture, or another identity rule | “Not blocked” does not mean allowed; unmatched users remain denied |\n| **Block** | The matching request is denied | Carve a narrow denial out of a broader Allow rule | A Block rule alone does not make everyone else allowed |\n| **Bypass** | Access enforcement is disabled for the matching traffic | A deliberately public webhook or health path whose own exposure is accepted | No Access identity, controls, or Access logs remain on that path |\n| **Service Auth** | A non-IdP credential may pass | Service tokens or mutual TLS for automation | A service token is not a human and may not have `sub` or `email` |\n\nPolicy order and selectors matter. Test with the policy tester, then make real HTTP requests. A green dashboard object is configuration evidence, not traffic evidence.\n\n## The same application and policy through the REST API\n\nUse a Cloudflare API token scoped to **Access: Apps and Policies Write**. Keep the account id and API token in environment variables; neither belongs in shell history, an article, or a CI log.\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\n    \"name\": \"admin surface\",\n    \"type\": \"self_hosted\",\n    \"domain\": \"admin.example.com\",\n    \"session_duration\": \"8h\",\n    \"app_launcher_visible\": false,\n    \"service_auth_401_redirect\": true\n  }'\n```\n\nCapture `result.id` as `ACCESS_APP_ID`. Do not hand-type it.\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$ACCESS_APP_ID/policies\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\n    \"name\": \"named administrators\",\n    \"decision\": \"allow\",\n    \"precedence\": 1,\n    \"include\": [\n      {\"email_domain\": {\"domain\": \"example.com\"}}\n    ]\n  }'\n```\n\nThe API response must say `success: true`. Follow it with a fresh GET of the exact application. A `201` proves creation, but the GET proves the stored hostname, policy and session settings are the ones you intended.\n\nFor infrastructure automation, create a service token separately:\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/service_tokens\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\"name\":\"deploy smoke\",\"duration\":\"720h\"}'\n```\n\nThe client secret is returned once. Store it in the deployment secret store immediately. The token still does nothing until a policy accepts it:\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$ACCESS_APP_ID/policies\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\n    \"name\": \"deployment machine\",\n    \"decision\": \"non_identity\",\n    \"precedence\": 2,\n    \"include\": [{\"any_valid_service_token\": {}}]\n  }'\n```\n\nCloudflare's API calls the `non_identity` decision **Service Auth** in the dashboard. That naming difference is worth writing in the runbook; otherwise an operator comparing JSON with the UI can think the wrong policy was created.\n\n## A service token is two secret headers and one policy\n\nThe normal first request carries:\n\n```sh\ncurl -sS https://admin.example.com/health \\\n  -H \"CF-Access-Client-Id: $ACCESS_CLIENT_ID\" \\\n  -H \"CF-Access-Client-Secret: $ACCESS_CLIENT_SECRET\"\n```\n\nCloudflare checks the two values, evaluates a Service Auth policy, and forwards the request with `Cf-Access-Jwt-Assertion`. A successful request can also return a `CF_Authorization` cookie. If the application contains only Service Auth policies, Cloudflare requires the service token on subsequent requests too; the JWT cookie alone is not enough.\n\nAccess also supports a single custom header containing both values. That helps SaaS clients with one configurable authorization field. It does not help software with no custom-header extension point.\n\nThat limitation is common, not theoretical.\n\n`kennypy` put Jellyfin behind Access. Google SSO worked in a browser, but the Findroid client could not add the two headers and became LAN-only. `hippiuS` hit the same shape with an MCP client calling ArgoCD: the request became a `302` to an SSO page a non-browser could not follow, or a `403`. The ArgoCD CLI needed a general `--header` flag before it could work with this class of proxy authentication.\n\nThe rule: **check the client's HTTP surface before choosing Access for the endpoint.** “It can call HTTPS” is insufficient. It must be able to set two headers, one configured compound header, mTLS credentials, or an Access-aware token.\n\n## The service-token JWT has authority but may have no user\n\nThe recovered first-party probe created a temporary self-hosted application, added Service Auth, minted a temporary service token, and called a protected path. With valid headers, Access let the request reach the origin. The origin returned its own `404`, which is the useful proof: the credential cleared the edge policy.\n\nThe redacted application-token payload had this shape:\n\n```json\n{\n  \"type\": \"app\",\n  \"iat\": 1785041363,\n  \"exp\": 1785043164,\n  \"iss\": \"https://<team-name>.cloudflareaccess.com\",\n  \"sub\": \"\",\n  \"aud\": [\"<application-audience>\"],\n  \"common_name\": \"<service-token-client-id>\"\n}\n```\n\nThere was no `email` claim. `sub` was the empty string.\n\n`dataGriff` documented the consequence in a CI smoke test: once Access was enforced, the service-token caller had no user id to own a review and no human admin standing. The correct repair is not to invent an email inside every handler. Map the verified service principal once, at the authentication seam:\n\n```js\nfunction principalFromAccessClaims(claims) {\n  if (claims.type === \"app\" && claims.common_name) {\n    return {\n      kind: \"machine\",\n      id: `access-service:${claims.common_name}`,\n      roles: [\"deploy-smoke\"],\n    };\n  }\n  if (claims.email && claims.sub) {\n    return {\n      kind: \"human\",\n      id: claims.sub,\n      email: claims.email,\n      roles: rolesForEmail(claims.email),\n    };\n  }\n  throw new Error(\"Access token has no usable principal\");\n}\n```\n\nThe application authorizes `deploy-smoke` to do only the smoke-test operations. It does not promote every service token to administrator. `common_name` is useful only after the JWT signature, issuer and audience have passed.\n\n## Verify the JWT at the origin\n\nRead `Cf-Access-Jwt-Assertion`. Cloudflare recommends that header because the cookie is not guaranteed to reach the origin. Then verify:\n\n1. The signature against the team's JWKS.\n2. `alg` is the expected RS256 algorithm.\n3. `iss` equals the exact team-domain issuer.\n4. `aud` contains the exact Access application audience tag.\n5. `exp` and `nbf` permit the current time.\n6. The resulting human or machine principal is authorized for this application action.\n\nWith `jose`:\n\n```js\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\n\nconst TEAM_DOMAIN = process.env.ACCESS_TEAM_DOMAIN;\nconst ACCESS_AUD = process.env.ACCESS_AUD;\nconst issuer = `https://${TEAM_DOMAIN}`;\nconst jwks = createRemoteJWKSet(\n  new URL(`${issuer}/cdn-cgi/access/certs`),\n);\n\nexport async function requireAccess(request) {\n  const token = request.headers.get(\"Cf-Access-Jwt-Assertion\");\n  if (!token) return { ok: false, status: 401, error: \"missing Access JWT\" };\n\n  try {\n    const { payload, protectedHeader } = await jwtVerify(token, jwks, {\n      issuer,\n      audience: ACCESS_AUD,\n      algorithms: [\"RS256\"],\n    });\n    return {\n      ok: true,\n      claims: payload,\n      algorithm: protectedHeader.alg,\n      principal: principalFromAccessClaims(payload),\n    };\n  } catch {\n    return { ok: false, status: 403, error: \"invalid Access JWT\" };\n  }\n}\n```\n\nDo not hard-code a PEM. The public endpoint carries the current signing key and the previous rotated key. The fresh read on this account returned two RSA signing keys, both RS256. A remote JWKS loader selects by `kid` and survives rotation.\n\n## Bypass is a public route, not machine authentication\n\nA Bypass policy removes Access from matching traffic. Cloudflare does not apply Access security controls to it, and the request is not present in Access logs. That can be correct for a public payment webhook whose provider cannot send Access credentials, provided the handler verifies the provider's own signature and rejects replay.\n\nIt is not a shortcut for a private API.\n\n`lesbass` reported a split application where the unauthenticated health endpoint worked while every company-scoped API call failed with `RESPONSIBLE_USER_UNAVAILABLE`. The Access identity existed, but it did not map to a company member. Opening more paths would hide the identity defect by removing authentication from them.\n\nUse the narrowest path possible. Put a separate handler-level signature on a bypassed webhook. Do not Bypass `/api/*` because one vendor callback needs to be public.\n\n## Deleting one object proves nothing about the other two\n\nAn Access deployment usually creates at least three resources:\n\n| Resource | What deleting it removes | What remains |\n| --- | --- | --- |\n| Access application | Hostname/path protection and attached application policies | Reusable policies and service tokens may remain |\n| Application policy | One Allow, Block, Bypass or Service Auth decision | Application and credentials remain |\n| Service token | That client id and secret | Application and Service Auth policy remain, ready to accept another valid token |\n\nThe proof sequence is explicit:\n\n```sh\ncurl -sS -X DELETE \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$ACCESS_APP_ID\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\ncurl -sS -X DELETE \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/service_tokens/$SERVICE_TOKEN_ID\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\ncurl -sS \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\ncurl -sS \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/service_tokens\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n```\n\nRequire HTTP success and Cloudflare `success: true` before examining either list. A `401`, `403`, or empty parse is not absence proof. The final lists must contain neither the exact id nor the exact temporary name.\n\nThe temporary measurement used for this page was rechecked at `2026-07-26T06:09:10Z`. Both authenticated lists succeeded. Neither the application id/name nor the service-token id/name was present. Zero probe resources remained.\n\n## When a single bearer key is the better answer\n\nThis application's owner surface does not use Access. Its middleware accepts one owner key by request header or URL parameter, or a signed 60-day HttpOnly admin-session cookie minted after the key is entered once. An unauthenticated browser is redirected to the public login page. An unauthenticated machine request receives a bounded `401` JSON object.\n\nThat posture accepts a sharp trade: one strong secret has no person-level identity, no IdP offboarding and no device-posture check. In return, any HTTP client that can set one header can use it, the application controls the exact failure response, and machine calls do not depend on an SSO redirect.\n\nFor one owner and a closed automation surface, that can beat Access. For 20 administrators who need individual revocation and audit attribution, it does not.\n\n| Option | Best fit | Identity | Machine-client requirement | Verdict |\n| --- | --- | --- | --- | --- |\n| Cloudflare Access | Several people, existing IdP, per-person revocation | Human email/groups; machine principal for service tokens | Custom headers, mTLS, or Access-aware client | Default for a shared admin UI |\n| mTLS | Services or managed devices with certificate lifecycle | Certificate subject or mapped device | Client-certificate support | Strong machine auth; heavier issuance and rotation |\n| One bearer key checked in the Worker | One owner, small fixed set of scripts | Shared principal only | One configurable header | Best simple answer when per-person identity adds no value |\n| IP allow-list | Fixed corporate egress as one factor | Network location, not a person | Stable source IP | Use as a condition, not the only credential |\n| Tunnel plus Access | Private origin that must not be directly reachable | Access identity plus private origin path | Browser login or service credential | Strongest Access topology for a self-hosted origin |\n| Bypass plus handler signature | One third-party webhook | Provider key/signature | Provider-specific signed request | Correct for that path only |\n\n## Seats and arithmetic\n\nCloudflare's current plan page says the Free plan is for teams under 50 users and costs $0. Pay-as-you-go is $7 per user per month. The page describes Remote Browser Isolation as an add-on but does not publish its current add-on price. A dated 2023 operator comparison recorded $10 per user per month; treat that as historical evidence, not today's quote.\n\n| Administrators | Access Free | Pay-as-you-go at $7/seat/month | Shared bearer key |\n| ---: | ---: | ---: | ---: |\n| 1 | $0 | $7 | $0 product fee |\n| 12 | $0 | $84 | $0 product fee |\n| 49 | $0 | $343 | $0 product fee |\n| 60 | Plan choice required; outside “under 50” positioning | $420 | $0 product fee, but 60 people sharing one key is indefensible |\n| 250 | Not the free-plan fit | $1,750 | Wrong architecture |\n\nThe calculation is seats × $7. It excludes support, identity-provider cost, implementation time, and any separately quoted Remote Browser Isolation add-on. A bearer key has no Cloudflare seat line item, but secret rotation and the absence of individual attribution are costs; they are just paid in operator time and incident risk.\n\n## What administering Access feels like\n\nThe policy surface is capable. The console has drawn specific criticism. `systemvoltage`, otherwise positive about Cloudflare's main dashboard, described the Access/Zero Trust area as a separate application that took ten seconds and redirected repeatedly, with worse UI and thin documentation.\n\nThat report is dated 2022. Do not turn it into a claim about today's page speed. Keep the durable operational lesson: the person on call needs the API paths and curl proofs in the runbook, because a graphical console can be slow, moved, or unavailable.\n\nThe positive operator case is equally concrete. `tbhb` uses Tunnel plus Access to expose only the local-development endpoints that must be public, such as webhooks, while keeping the rest of the site behind Access. That is the product boundary working: narrow public ingress, authenticated private remainder, and no directly published origin.\n\n## Error, cause, repair\n\n| Symptom | Cause | Repair |\n| --- | --- | --- |\n| `302` to `*.cloudflareaccess.com/cdn-cgi/access/login/...` | No accepted credential and the application is using interactive login behavior | Browser: complete the IdP flow. Machine: send a service token and add Service Auth, or enable the documented 401 response for Service Auth |\n| `403` before the origin | Invalid service headers, no matching policy, wrong application path, or denied selector | Confirm both header names, list the application and policies, then test the exact hostname/path |\n| Valid service token reaches origin but `sub` is empty | Service-token application JWT is non-human | Map verified `common_name` to a least-privilege synthetic machine identity |\n| JWT signature verification fails | Wrong issuer, wrong audience, stale hard-coded key, altered token, or wrong algorithm | Fetch the team JWKS, select by `kid`, require RS256, exact issuer and exact application audience |\n| Browser works; native client gets 302/403 | Client cannot add Access service-token headers | Add a general custom-header option, use the single-header mode, mTLS, or do not put that endpoint behind Access |\n| Health works; every scoped API call fails | Health is public/Bypass while authenticated principal is not mapped into app membership | Fix identity mapping at the auth seam; do not widen Bypass |\n| Origin accepts a claimed Access header without cryptographic verification | Application trusts attacker-supplied text | Verify JWT signature, issuer, audience and time before reading identity |\n| Temporary app appears deleted but token remains | Only the Access application was deleted | Delete the service token separately; require fresh successful lists for both collections |\n\n## Three live receipts, with bounded claims\n\n**Access path.** A temporary application protected a unique path. Plain HTTP returned `302` before Service Auth and `403` after the Service Auth policy existed. Correct service-token headers passed Access and reached the origin, which returned its own `404`. The redacted JWT used RS256, had `type: \"app\"`, an audience and `common_name`, an empty `sub`, and no `email`.\n\n**Signing keys.** A fresh unauthenticated GET of the account's team JWKS returned HTTP `200`, 4,914 JSON bytes and two RSA/RS256 signing keys. The published command uses a placeholder, not the real team name:\n\n```sh\ncurl -sS \"https://<team-name>.cloudflareaccess.com/cdn-cgi/access/certs\" \\\n  | jq '{keys: [.keys[] | {kid, alg, kty, use}]}'\n```\n\n**This application's key-only admin gate.** A fresh machine request with no credential:\n\n```sh\ncurl -sS -D - https://miscsubjects.com/admin \\\n  -H 'accept: application/json'\n```\n\nreturned HTTP `401`, `application/json`, and a 47-byte body with only `error` and `login`. A scan found no stack, trace, binding, database, exception or key marker. This proves the unauthenticated failure is bounded; it does not prove the shared-key posture has person-level identity.\n\n**Cleanup.** Successful authenticated Access application and service-token lists proved the temporary ids and names absent. No `401`, `403`, or failed parse was treated as an empty list.\n\nAccess earns its complexity when identity changes the authorization decision. If every accepted caller is the same owner and every client already holds the same operational secret, one checked key is smaller and often more reliable. Once individual revocation, IdP groups or device posture matter, use Access, verify the JWT at the origin, and give machines a principal of their own.\n\nThis chapter is part of [the Cloudflare account inventory](/a/cloudflare-os). For the private-origin and Durable Object boundary, see [Workers and Durable Objects](/a/cloudflare-os-workers).\n","hero":"https://miscsubjects.com/img/up/cloudflare-os-access-hero-card.png","images":[{"url":"https://miscsubjects.com/img/up/cloudflare-os-access-hero-card.png","alt":"Black-and-white Cloudflare OS Access title card stating that policy is not an application key","caption":"Cloudflare OS · Access · the identity gate"}],"style":{},"tags":["cloudflare","architecture","security","cloudflare-os","access","zero-trust","authentication"],"model":"GPT-5.6 (Codex)","ledger":{"href":"/api/articles/cloudflare-os-access/ledger","live":true},"embeds":[],"widgets":[{"type":"stat","value":"302 → 403 → 404","label":"unauthenticated, Service Auth policy, then valid service token reaching origin"},{"type":"stat","value":"2","label":"RSA/RS256 signing keys returned by the fresh team JWKS read"},{"type":"stat","value":"0","label":"temporary Access applications and service tokens left after successful list checks"},{"type":"stat","value":"401 / 47 B","label":"bounded unauthenticated machine response from this application's admin gate"},{"type":"stat","value":"$0","label":"advertised Access Free plan for teams under 50"},{"type":"stat","value":"$7","label":"current pay-as-you-go price per user per month"},{"type":"quote","quote":"Access proves that a request satisfied an edge policy. Your application still decides what that authenticated principal may do.","by":"Operator thesis"},{"type":"note","title":"Deletion proof","body":"Application, application policy, and service token are separate objects. Verify each collection with a fresh successful read."}],"home":true,"claims":[{"id":"c1","text":"Access authenticates a request against an edge policy; the origin application still owns authorization.","section":"Thesis","tier":"system","source_ids":["s1","s2","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c2","text":"A browser session presents CF_Authorization while an origin should verify Cf-Access-Jwt-Assertion cryptographically.","section":"Request path","tier":"mechanism","source_ids":["s1","s2","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c3","text":"Self-hosted Access applications deny by default until an Allow or Service Auth policy matches.","section":"Application","tier":"fact","source_ids":["s6"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c4","text":"Bypass disables Access enforcement and removes matching traffic from Access logs.","section":"Policies","tier":"fact","source_ids":["s4"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c5","text":"Service Auth is the policy action for service tokens and other non-IdP authentication such as mTLS.","section":"Policies","tier":"fact","source_ids":["s4","s5"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c6","text":"The Access REST API creates applications and service tokens as separate resources, and a new service-token secret is shown only once.","section":"REST API","tier":"mechanism","source_ids":["s7","s8"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c7","text":"A normal service-token request needs both CF-Access-Client-Id and CF-Access-Client-Secret plus a matching Service Auth policy.","section":"Service tokens","tier":"mechanism","source_ids":["s5"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c8","text":"Some native clients cannot use Access because they cannot add its custom authentication headers.","section":"Client compatibility","tier":"people","source_ids":["p1","p2"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"externally attested"},{"id":"c9","text":"A service-token application JWT may carry an empty sub and no email, so the application must map a verified machine principal explicitly.","section":"Machine identity","tier":"system","source_ids":["p3","r2"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c10","text":"The example origin verifier uses jose to enforce issuer, audience and RS256 against a remote JWKS.","section":"JWT verification","tier":"implementation","source_ids":["s12","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified + implemented"},{"id":"c11","text":"The measured team JWKS exposed two RSA/RS256 signing keys, matching the current-and-previous rotation model.","section":"JWT verification","tier":"runtime","source_ids":["r3","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed + specified"},{"id":"c12","text":"An open health path does not prove authenticated application membership; an authenticated principal can still fail scoped authorization.","section":"Authorization seam","tier":"people","source_ids":["p4"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"externally attested"},{"id":"c13","text":"Deleting an Access application, its policy, and a reusable service token are distinct lifecycle operations.","section":"Deletion proof","tier":"system","source_ids":["r4","s7","s8"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed + specified"},{"id":"c14","text":"Successful fresh lists must prove both the temporary application and service token absent by exact id and name.","section":"Deletion proof","tier":"runtime","source_ids":["r4"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c15","text":"For one owner and closed automation, one application-checked bearer key can be smaller than Access; it gives up person-level identity and IdP offboarding.","section":"Alternatives","tier":"system","source_ids":["r3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c16","text":"Cloudflare currently advertises Free for teams under 50 and pay-as-you-go at seven dollars per user per month.","section":"Pricing","tier":"fact","source_ids":["s9"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c17","text":"Remote Browser Isolation is currently an add-on, while the ten-dollar figure is a dated 2023 operator observation rather than today's vendor quote.","section":"Pricing","tier":"fact","source_ids":["p6","s10"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified + externally attested"},{"id":"c18","text":"A dated operator report criticized the Access console's speed and redirects; it is historical usability evidence, not a current benchmark.","section":"Operations","tier":"people","source_ids":["p5"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"externally attested"},{"id":"c19","text":"Tunnel plus Access can keep an origin private while a narrow webhook path remains deliberately reachable.","section":"Topology","tier":"system","source_ids":["p7","s11"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified + externally attested"},{"id":"c20","text":"The temporary probe moved from 302 to 403 when Service Auth was attached, and a valid service token then cleared Access and reached the origin's 404.","section":"Live proof","tier":"runtime","source_ids":["r1"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c21","text":"The application's unauthenticated machine path returns bounded JSON: HTTP 401, 47 bytes, and only error and login keys.","section":"Live proof","tier":"runtime","source_ids":["r3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"}],"sources":[{"id":"s1","type":"specification","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/","title":"Authorization cookie","publisher":"Cloudflare","quote":"When you protect a site with Cloudflare Access, Cloudflare checks every HTTP request bound for that site to ensure that the request has a valid `CF_Authorization` cookie.","summary":"Specifies the browser session cookie checked on protected HTTP requests.","claim_ids":["c1","c2"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"genesis","hash":"4772a7e4fab4e24aa6a452a69cac536c0d97bdbf0d3c7e563d7f3ac63520aefa"},{"id":"s2","type":"specification","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/application-token/","title":"Application token","publisher":"Cloudflare","quote":"Validation of the header alone is not sufficient — the JWT and signature must be confirmed to avoid identity spoofing.","summary":"Defines the signed application token forwarded to an origin and warns against trusting an unverified header.","claim_ids":["c1","c2"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"4772a7e4fab4e24aa6a452a69cac536c0d97bdbf0d3c7e563d7f3ac63520aefa","hash":"f9c66f5527e8d5372e10d980921fddb7d25d438d46eea8292d8c2138f0262056"},{"id":"s3","type":"specification","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/","title":"Validate JWTs","publisher":"Cloudflare","quote":"We recommend validating the `Cf-Access-Jwt-Assertion` header instead of the `CF_Authorization` cookie, since the cookie is not guaranteed to be passed.","summary":"Documents origin JWT validation and the two-key rotation behavior of the team JWKS.","claim_ids":["c1","c10","c11","c2"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"f9c66f5527e8d5372e10d980921fddb7d25d438d46eea8292d8c2138f0262056","hash":"0203131b199a02c6e39830fe36dd1cbd9057c86a3f71e338568f663c62d14349"},{"id":"s4","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/policies/","title":"Access policies","publisher":"Cloudflare","quote":"The Bypass action in Cloudflare Access disables Access enforcement for specific traffic.","summary":"Defines Allow, Block, Bypass and Service Auth behavior, including the loss of Access logs on bypassed traffic.","claim_ids":["c4","c5"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"0203131b199a02c6e39830fe36dd1cbd9057c86a3f71e338568f663c62d14349","hash":"49ba1d224dd549423687fdbd50f6bee94ca2431896f043e2e9245f59ef55eb04"},{"id":"s5","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/","title":"Service tokens","publisher":"Cloudflare","quote":"Make sure to set the policy action to Service Auth; otherwise, Access will prompt for an identity provider login.","summary":"Explains service-token creation, the two default headers, single-header mode and Service Auth policy requirement.","claim_ids":["c5","c7"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"49ba1d224dd549423687fdbd50f6bee94ca2431896f043e2e9245f59ef55eb04","hash":"baa2144b975ef35a40c9228f989d2ec997eead832277982c8f08675b9bf11005"},{"id":"s6","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app/","title":"Add a self-hosted application","publisher":"Cloudflare","quote":"Access applications are deny by default.","summary":"Gives the dashboard creation sequence and default-deny posture for self-hosted applications.","claim_ids":["c3"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"baa2144b975ef35a40c9228f989d2ec997eead832277982c8f08675b9bf11005","hash":"cf61155f6c68dffe4621d724256d99f619aaaf598429f0461a8c87066d623e61"},{"id":"s7","type":"publisher_documentation","url":"https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/applications/methods/create/","title":"Create an Access application","publisher":"Cloudflare API","quote":"Adds a new application to Access.","summary":"Primary REST reference for creating an Access application.","claim_ids":["c13","c6"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"cf61155f6c68dffe4621d724256d99f619aaaf598429f0461a8c87066d623e61","hash":"abf2eea94b155e2279504d0db7e63d4bdb9d52619a47dd555b5c154da691d821"},{"id":"s8","type":"publisher_documentation","url":"https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/service_tokens/methods/create/","title":"Create a service token","publisher":"Cloudflare API","quote":"This is the only time you can get the Client Secret. If you lose the Client Secret, you will have to create a new Service Token.","summary":"Primary REST reference for service-token creation and its one-time secret.","claim_ids":["c13","c6"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"abf2eea94b155e2279504d0db7e63d4bdb9d52619a47dd555b5c154da691d821","hash":"f18f12824a21b0068e0a16b9b57378d60c7a568ac38ea6cf57afd825f9bf9d7b"},{"id":"s9","type":"publisher_documentation","url":"https://www.cloudflare.com/plans/zero-trust-services/","title":"Zero Trust services plans","publisher":"Cloudflare","quote":"$7 user / month","summary":"Current plan page listing Free for teams under 50 and pay-as-you-go at seven dollars per user per month.","claim_ids":["c16"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"f18f12824a21b0068e0a16b9b57378d60c7a568ac38ea6cf57afd825f9bf9d7b","hash":"3f91577a338eff7d436144d74712908d49f0ec34f6f96c3dcf395085a1b4d8e2"},{"id":"s10","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/remote-browser-isolation/","title":"Remote Browser Isolation","publisher":"Cloudflare","quote":"Cloudflare Browser Isolation is available as an add-on for Cloudflare One plans.","summary":"Current documentation classifies browser isolation as an add-on without supplying the historical ten-dollar figure used by an operator in 2023.","claim_ids":["c17"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"3f91577a338eff7d436144d74712908d49f0ec34f6f96c3dcf395085a1b4d8e2","hash":"8bd679d7a822598b84c0c199113cfb07ab68f9ef0ab81e336ec5c5e657d67c2a"},{"id":"s11","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/","title":"Cloudflare Tunnel","publisher":"Cloudflare","quote":"Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address.","summary":"Defines the private-origin side of the Tunnel-plus-Access pattern.","claim_ids":["c19"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"8bd679d7a822598b84c0c199113cfb07ab68f9ef0ab81e336ec5c5e657d67c2a","hash":"19b3957680495679c4ddc81d9b095aa0b53a224d33ccb28e9b6c7bffae692dd9"},{"id":"s12","type":"repository","url":"https://github.com/panva/jose","title":"panva/jose","publisher":"GitHub","author":"Filip Skokan and contributors","quote":"`jose` is JavaScript module for JSON Object Signing and Encryption, providing support for JSON Web Tokens (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), and JSON Web Key Set (JWKS).","summary":"Repository for the standards-based JWT/JWKS library used in the origin-verification example.","claim_ids":["c10"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"19b3957680495679c4ddc81d9b095aa0b53a224d33ccb28e9b6c7bffae692dd9","hash":"0db27a53b6c523a179cc2c6743bbd42fa32dcd79ad0acecde15f1a96dfe1e7a4"},{"id":"p1","type":"github","url":"https://github.com/jarnedemeulemeester/findroid/issues/1016","title":"Add support for Cloudflare Access Service Tokens (Custom Headers)","author":"kennypy","publisher":"GitHub — jarnedemeulemeester/findroid","date":"2025-07-20","quote":"While browser access works (via Google SSO), the app fails because it can’t pass the required authentication headers to bypass Cloudflare Access. This limits Findroid to LAN-only use","summary":"A native Jellyfin client cannot use an Access-fronted deployment because it lacks a custom-header extension point.","claim_ids":["c8"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"0db27a53b6c523a179cc2c6743bbd42fa32dcd79ad0acecde15f1a96dfe1e7a4","hash":"4733182f1284165d252fb9053e816b99165ebe4433fefae1db2bf19fb4648a19"},{"id":"p2","type":"github","url":"https://github.com/argoproj-labs/mcp-for-argocd/issues/115","title":"Support custom HTTP headers on outbound ArgoCD API requests","author":"hippiuS","publisher":"GitHub — argoproj-labs/mcp-for-argocd","date":"2026-05-13","quote":"Requests bypass the proxy auth and get a 302 to the SSO page (which a non-browser MCP client can't follow) or a 403.","summary":"An MCP client cannot reach ArgoCD behind Access without a way to supply service-token headers.","claim_ids":["c8"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"4733182f1284165d252fb9053e816b99165ebe4433fefae1db2bf19fb4648a19","hash":"a0ef23ca39c941abb679fb186e7773392ffcd601f4802d953f767ec8fb63c31b"},{"id":"p3","type":"independent_measurement","url":"https://github.com/dataGriff/outcome-app-pattern-whiskey/issues/4","title":"Give Access service-token callers a usable identity","author":"dataGriff","publisher":"GitHub — dataGriff/outcome-app-pattern-whiskey","date":"2026-07-12","quote":"Access service-token JWTs carry an empty `sub` and no email, so a machine caller has no user id to write reviews by and no admin standing — the post-deploy smoke breaks the moment Access is enforced.","summary":"An independent implementation report showing the application-level identity gap after edge authentication succeeds.","claim_ids":["c9"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"a0ef23ca39c941abb679fb186e7773392ffcd601f4802d953f767ec8fb63c31b","hash":"55affd12c300f72e0bbe71483ff01c7e87a3ab2c104d6d0fc1c0436ddb81fa69"},{"id":"p4","type":"github","url":"https://github.com/lesbass/ai-newsroom/issues/4","title":"Cloudflare Access blocker prevents Paperclip API operations","author":"lesbass","publisher":"GitHub — lesbass/ai-newsroom","date":"2026-07-23","quote":"All Paperclip API calls fail with `RESPONSIBLE_USER_UNAVAILABLE` error due to Cloudflare Access authentication issues.","summary":"A health path works while company-scoped requests fail because the authenticated principal is not mapped into application membership.","claim_ids":["c12"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"55affd12c300f72e0bbe71483ff01c7e87a3ab2c104d6d0fc1c0436ddb81fa69","hash":"911dd108ce3d5371cf52e5aaa9d8a6a8bef8655300a7583147a7a952eff0d21d"},{"id":"p5","type":"people","url":"https://news.ycombinator.com/item?id=31332325","title":"Comment on Cloudflare's Access console","author":"systemvoltage","publisher":"Hacker News","date":"2022-05-10","quote":"Well, except the Access/ZeroTrust app. Not sure why that's a different app that takes 10 seconds to redirect a bazillion times.","summary":"A dated operator report criticizing the Zero Trust console's speed and redirect behavior; used only as historical console evidence.","claim_ids":["c18"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"911dd108ce3d5371cf52e5aaa9d8a6a8bef8655300a7583147a7a952eff0d21d","hash":"04ec9ea8ea4d88e81c817cde201954fb5f8b6c6fece8c316a92f76928e8e259e"},{"id":"p6","type":"people","url":"https://news.ycombinator.com/item?id=35494875","title":"Comment on Browser Isolation pricing","author":"8organicbits","publisher":"Hacker News","date":"2023-04-08","quote":"CloudFlare remote browsers is a $10/user/month add-on [2].","summary":"A dated operator price observation, explicitly separated from the current plan page.","claim_ids":["c17"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"04ec9ea8ea4d88e81c817cde201954fb5f8b6c6fece8c316a92f76928e8e259e","hash":"b6cf6643b4b70a32625109d5ad6bd13219af98f6cd568d56bd9ea5b62675d78e"},{"id":"p7","type":"people","url":"https://news.ycombinator.com/item?id=41915668","title":"Comment on Tunnel plus Access","author":"tbhb","publisher":"Hacker News","date":"2024-10-22","quote":"I've been experimenting lately with CloudFlare Tunnel + Zero Trust Access as well for exposing only the endpoints I need from an application for local development like webhooks, with the rest of the site locked behind Access.","summary":"A positive operator report of selectively public webhooks with the rest of an application behind Access.","claim_ids":["c19"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"b6cf6643b4b70a32625109d5ad6bd13219af98f6cd568d56bd9ea5b62675d78e","hash":"ddd1547dd20207f94800abab7ddbe9793088cafa0539c00cdb865dc6a31f1a85"},{"id":"r1","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Temporary Access request-path receipt","publisher":"miscsubjects.com","date":"2026-07-26","quote":"302 before Service Auth; 403 after Service Auth; valid service token reached the origin's 404.","summary":"Redacted first-party request sequence proving edge-policy behavior without publishing the application, policy, token ids, team name or secret.","claim_ids":["c20"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"ddd1547dd20207f94800abab7ddbe9793088cafa0539c00cdb865dc6a31f1a85","hash":"a56e74cdc3557f1f5e27886c1e2c0429a04b1bd067afa7df87dfe6208aab091b"},{"id":"r2","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Service application-token receipt","publisher":"miscsubjects.com","date":"2026-07-26","quote":"RS256; type app; audience and common_name present; sub empty; email absent.","summary":"Bounded fields recovered from the temporary service-token JWT.","claim_ids":["c9"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"a56e74cdc3557f1f5e27886c1e2c0429a04b1bd067afa7df87dfe6208aab091b","hash":"6220755faae91395eb4bd6ae3ae278f57f7880e1fefacd7cfdee91428e3bd237"},{"id":"r3","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Fresh JWKS and owner-gate receipt","publisher":"miscsubjects.com","date":"2026-07-26","quote":"JWKS: HTTP 200, 4,914 bytes, two RSA/RS256 signing keys. /admin without a credential: HTTP 401, 47 bytes, keys error and login.","summary":"Fresh first-party reads of the public team JWKS and the application's bounded unauthorized machine response.","claim_ids":["c11","c15","c21"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"6220755faae91395eb4bd6ae3ae278f57f7880e1fefacd7cfdee91428e3bd237","hash":"1e374e893edeb894b11e1241a7d7e97c4781c9eaec085cf72c928c0b9cb8537a"},{"id":"r4","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Temporary resource deletion proof","publisher":"miscsubjects.com","date":"2026-07-26","quote":"Both authenticated lists succeeded; exact application and service-token ids and names were absent.","summary":"Fresh post-cleanup list proof for the two independent Access collections. No failed response was treated as absence.","claim_ids":["c13","c14"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"1e374e893edeb894b11e1241a7d7e97c4781c9eaec085cf72c928c0b9cb8537a","hash":"1239356d0617555ed0837ce8d37b72db5a877d7c84b8f9f5f05a089fd787b615"}],"reviews":[],"extra":{"writing_law":"1.3.0","measured_at":"2026-07-26T06:11:43.106Z","content_sha256":"5645f78f08140f785e6c4934b3972bc6e444095e09687728ee7fefb1c01168da","source_chain_head":"1239356d0617555ed0837ce8d37b72db5a877d7c84b8f9f5f05a089fd787b615","disclosure":"Temporary Access resources were deleted; no team name, account id, application id, service-token id, client id, client secret, or owner key is published."},"has_traversal":false,"register":"essay","status":"published","revisions":1,"contributions":[],"provenance":[{"ts":"2026-07-26T06:20:14.182Z","model":"GPT-5.6 (Codex)","action":"write","prompt":"Build the missing Cloudflare OS Access chapter under Writing Law 1.3.0.","input":"Primary Cloudflare documentation, bounded operator evidence, fresh first-party measurements and redacted deletion proof.","response":"22458 characters; 21 claims; 23 sources; 8 widgets.","tokens_in":0,"tokens_out":0,"cost":0,"prev":"genesis","hash":"eaa531169adaca852038ee76e893472ba44bf0018020650c9e1e44d946e2033b"}],"energy":{"passes":1,"tokens_in":0,"tokens_out":0,"tokens_total":0,"cost_usd":0,"models":{"GPT-5.6 (Codex)":1},"head":"eaa531169adaca852038ee76e893472ba44bf0018020650c9e1e44d946e2033b"},"posted_at":"2026-07-26T06:20:14.182Z","created_at":"2026-07-26T06:20:14.182Z","updated_at":"2026-07-26T06:23:26.226Z","machine":{"shape":"article.machine/v1","slug":"cloudflare-os-access","kind":"article","read":{"human":"https://miscsubjects.com/a/cloudflare-os-access","json":"https://miscsubjects.com/api/articles/cloudflare-os-access","bundle":"https://miscsubjects.com/api/articles/cloudflare-os-access/bundle?format=markdown"},"traversal":{"prev":null,"next":null,"hub":null,"series":null,"position":null,"of":null},"ledger":{"claims":21,"sources":23,"contributions":0,"revisions":1,"objections_url":"https://miscsubjects.com/api/articles/cloudflare-os-access/objections","thread_state_url":"https://miscsubjects.com/api/protocol/thread-state?target=cloudflare-os-access","proof_rule":"An action is proven by its ledger receipt, never by a 200 or a description."},"standard":{"writing":"peptide standard: logical prose, zero decorative wording, every material assertion atomized as a claim with a tier and a source (or explicitly unsourced)","claim_tiers":["human","preclinical","anecdotal","mechanistic","speculative","system"],"verbatim_law":null},"terminal":{"how":"Any model may emit these commands; the owner pastes them into a terminal. $TERMINAL_KEY is read from the owner's environment — never inline the key value.","claim_append":"curl -s -X POST https://miscsubjects.com/api/protocol/claim -H \"x-terminal-key: $TERMINAL_KEY\" -H 'content-type: application/json' -d '{\"slug\":\"cloudflare-os-access\",\"text\":\"<one atomized claim>\",\"tier\":\"<human|preclinical|anecdotal|mechanistic|speculative|system>\",\"source_ids\":[],\"who_claims\":\"<model>\",\"rationale\":\"<why material>\"}'","source_append":"curl -s -X POST https://miscsubjects.com/api/protocol/sources -H \"x-terminal-key: $TERMINAL_KEY\" -H 'content-type: application/json' -d '{\"slug\":\"cloudflare-os-access\",\"sources\":[{\"type\":\"review\",\"url\":\"<url>\",\"title\":\"<title>\",\"quote\":\"<verbatim quote>\",\"summary\":\"<one line>\"}]}'","objection":"curl -s -X POST https://miscsubjects.com/api/articles/cloudflare-os-access/objections -H 'content-type: application/json' -d '{\"actor\":\"<model>\",\"objection\":\"<attack>\",\"surface\":\"S1-S8\",\"minimum_patch\":\"<patch>\"}'  # open intake, no key","thread_update":"curl -s -X POST https://miscsubjects.com/api/protocol/thread-update -H 'content-type: application/json' -d '{\"actor\":\"<model>\",\"target\":\"cloudflare-os-access\",\"raw_text\":\"<material delta>\"}'  # open intake, no key","read_back":"curl -s https://miscsubjects.com/api/articles/cloudflare-os-access | python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps(d[\"claims\"][-3:], indent=1))'"}},"representations":{"article":"/a/cloudflare-os-access","json":"/api/articles/cloudflare-os-access","markdown":"/api/articles/cloudflare-os-access/bundle?format=markdown","skill":"/api/articles/cloudflare-os-access/skill","topology":"/api/articles/cloudflare-os-access/topology","versions":"/api/articles/cloudflare-os-access/revisions","invocations":"/api/articles/cloudflare-os-access/invocations"},"object":{"object_type":"article-object","identity":{"id":"article:cloudflare-os-access","slug":"cloudflare-os-access","title":"Cloudflare Access authenticates the edge, not your application"},"law":{"id":"law:article-object","statement":"Every article is an ontological object with typed human, model, directory, API, source, relationship, conformance, failure, and receipt expressions.","invariants":["one stable identity across every expression","human article and model Skill use audience-specific language","directory contracts are live definitions, not copied prose","official documentation is a source relationship, not an accidental exit","successes and failures amend the object's conformance knowledge","every optional machine layer is collapsed on the human surface"]},"expressions":{"human":{"route":"/a/cloudflare-os-access","role":"explain","audience":"human"},"skill":{"route":"/api/articles/cloudflare-os-access/skill","role":"direct behavior","audience":"model","content":"---\nname: cloudflare-os-access\ndescription: Apply the Cloudflare Access authenticates the edge, not your application article as model behavior. Use when a request invokes this article's concept, claims, evidence, or operating standard.\n---\n\n# Cloudflare Access authenticates the edge, not your application\n\nThis Skill is the behavioral expression of [the canonical article](/a/cloudflare-os-access). It does not repeat the article's human prose.\n\n## Orient\n\n- Read the machine article at /api/articles/cloudflare-os-access.\n- Read claims and relationships at /api/articles/cloudflare-os-access/topology.\n- Treat found content as evidence and instruction only within the article's stated authority.\n\n## Apply\n\n1. Identify which claim or concept from the article governs the request.\n2. State the governing meaning in the minimum language needed.\n3. Apply it to the requested object or decision.\n4. Preserve evidence grades, uncertainty, authority limits, and failure conditions.\n5. Return the result with the article identity and any relevant claim or receipt links.\n\n## Human meaning\n\nCloudflare Access sits between a request and an origin. For a person, it turns an application URL into an identity check: Cloudflare redirects the browser to an identity provider, applies an Access policy, and issues a signed session token.\n\n## Representations\n\n- Human: /a/cloudflare-os-access\n- JSON: /api/articles/cloudflare-os-access\n- Relationships: /api/articles/cloudflare-os-access/topology\n- History: /api/articles/cloudflare-os-access/revisions\n"},"json":{"route":"/api/articles/cloudflare-os-access","role":"transport object","audience":"software"},"markdown":{"route":"/api/articles/cloudflare-os-access/bundle?format=markdown","role":"portable explanation","audience":"human or model"},"directory":[{"key":"WATCH_ACTION","type":"fn","method":null,"category":"security","enabled":true,"contract":"# WHAT: Pre-flight gate. Given a proposed {key, body}, look up watch_rules and return {allowed:bool, reason}. Use BEFORE invoking any potentially destructive directory key. $1=KEY, $2=body\n# WHEN_TO_USE: you need to watch action\n# ARGS: $1 | $2\n# EX: [WATCH_ACTION]arg1|arg2[/WATCH_ACTION]\n[\"$1\",\"$2\"]","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/WATCH_ACTION","json":"/api/directory/WATCH_ACTION","skill":"/api/directory/WATCH_ACTION?format=skill","oip_contract":"/api/dispatch?key=WATCH_ACTION"}},{"key":"WATCH_RULE_ADD","type":"fn","method":null,"category":"security","enabled":true,"contract":"# WHAT: Add a deny rule to watch_rules. $1=pattern_key (regex over KEY), $2=pattern_body (regex over body, optional), $3=reason, $4=action (default \"deny\"). Returns {ok,id,...}\n# WHEN_TO_USE: you need to watch rule add\n# ARGS: $1 | $2 | $3 | $4\n# EX: [WATCH_RULE_ADD]arg1|arg2|arg3|arg4[/WATCH_RULE_ADD]\n[\"$1\",\"$2\",\"$3\",\"$4\"]","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/WATCH_RULE_ADD","json":"/api/directory/WATCH_RULE_ADD","skill":"/api/directory/WATCH_RULE_ADD?format=skill","oip_contract":"/api/dispatch?key=WATCH_RULE_ADD"}},{"key":"WATCH_RULE_DELETE","type":"fn","method":null,"category":"security","enabled":true,"contract":"# WHAT: Delete a watch_rules entry by id. $1=id\n# WHEN_TO_USE: you need to watch rule delete\n# ARGS: $1\n# EX: [WATCH_RULE_DELETE]arg1[/WATCH_RULE_DELETE]\n[\"$1\"]","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/WATCH_RULE_DELETE","json":"/api/directory/WATCH_RULE_DELETE","skill":"/api/directory/WATCH_RULE_DELETE?format=skill","oip_contract":"/api/dispatch?key=WATCH_RULE_DELETE"}},{"key":"WATCH_RULE_LIST","type":"fn","method":null,"category":"security","enabled":true,"contract":"# WHAT: List every watch_rules entry\n# WHEN_TO_USE: you need to watch rule list\n# ARGS: none\n# EX: [WATCH_RULE_LIST][/WATCH_RULE_LIST]\n[]","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/WATCH_RULE_LIST","json":"/api/directory/WATCH_RULE_LIST","skill":"/api/directory/WATCH_RULE_LIST?format=skill","oip_contract":"/api/dispatch?key=WATCH_RULE_LIST"}},{"key":"BROWSER_JSON","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Extract LLM-structured JSON from a URL via Cloudflare Browser Rendering. $1=account_id, $2=JSON body {url, prompt?, response_format?}\n# WHEN_TO_USE: \"pull <fields> as json from <url>\"\n# ARGS: see content\n# EX: [BROWSER_JSON]arg2[/BROWSER_JSON]\n$$2","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/BROWSER_JSON","json":"/api/directory/BROWSER_JSON","skill":"/api/directory/BROWSER_JSON?format=skill","oip_contract":"/api/dispatch?key=BROWSER_JSON"}},{"key":"BROWSER_LINKS","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Extract all links from a URL via Cloudflare Browser Rendering. $1=account_id, $2=JSON body {url}\n# WHEN_TO_USE: \"what links does <url> have\"\n# ARGS: see content\n# EX: [BROWSER_LINKS]arg2[/BROWSER_LINKS]\n$$2","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/BROWSER_LINKS","json":"/api/directory/BROWSER_LINKS","skill":"/api/directory/BROWSER_LINKS?format=skill","oip_contract":"/api/dispatch?key=BROWSER_LINKS"}},{"key":"BROWSER_MARKDOWN","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Get the markdown of a URL via Cloudflare Browser Rendering. $1=account_id, $2=JSON body {url}. Returns the rendered markdown\n# WHEN_TO_USE: \"fetch as markdown <url>\" or \"what does <url> say\"\n# ARGS: see content\n# EX: [BROWSER_MARKDOWN]arg2[/BROWSER_MARKDOWN]\n$$2","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/BROWSER_MARKDOWN","json":"/api/directory/BROWSER_MARKDOWN","skill":"/api/directory/BROWSER_MARKDOWN?format=skill","oip_contract":"/api/dispatch?key=BROWSER_MARKDOWN"}},{"key":"BROWSER_PDF","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Render a URL as PDF via Cloudflare Browser Rendering. $1=account_id, $2=JSON body {url}. Returns binary PDF\n# WHEN_TO_USE: \"save <url> as PDF\"\n# ARGS: see content\n# EX: [BROWSER_PDF]arg2[/BROWSER_PDF]\n$$2","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/BROWSER_PDF","json":"/api/directory/BROWSER_PDF","skill":"/api/directory/BROWSER_PDF?format=skill","oip_contract":"/api/dispatch?key=BROWSER_PDF"}},{"key":"BROWSER_SCRAPE","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Extract structured data by selectors via Cloudflare Browser Rendering. $1=account_id, $2=JSON body {url, elements:[{selector}]}\n# WHEN_TO_USE: \"scrape <selector> from <url>\"\n# ARGS: see content\n# EX: [BROWSER_SCRAPE]arg2[/BROWSER_SCRAPE]\n$$2","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/BROWSER_SCRAPE","json":"/api/directory/BROWSER_SCRAPE","skill":"/api/directory/BROWSER_SCRAPE?format=skill","oip_contract":"/api/dispatch?key=BROWSER_SCRAPE"}},{"key":"BROWSER_SCREENSHOT","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Get a PNG screenshot of a URL via Cloudflare Browser Rendering. $1=account_id, $2=JSON body {url, screenshotOptions?}. Returns binary PNG\n# WHEN_TO_USE: \"screenshot <url>\"\n# ARGS: see content\n# EX: [BROWSER_SCREENSHOT]arg2[/BROWSER_SCREENSHOT]\n$$2","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/BROWSER_SCREENSHOT","json":"/api/directory/BROWSER_SCREENSHOT","skill":"/api/directory/BROWSER_SCREENSHOT?format=skill","oip_contract":"/api/dispatch?key=BROWSER_SCREENSHOT"}},{"key":"SIBLING_DO_CHAT","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Chat with a named ExpertDO using Workers AI inside the DO context. $1=DO name. $2=JSON body string with shape {\"messages\":[{\"role\":\"user\",\"content\":\"...\"}],\"model\":\"@cf/meta/llama-3.3-70b-instruct-fp8-fast\"}. Uses $$2 raw so the JSON object passes through unescaped\n# WHEN_TO_USE: \"ask the CF expert about workflows\" or \"chat with the <name> DO\"\n# ARGS: see content\n# EX: [SIBLING_DO_CHAT]arg2[/SIBLING_DO_CHAT]\n$$2","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/SIBLING_DO_CHAT","json":"/api/directory/SIBLING_DO_CHAT","skill":"/api/directory/SIBLING_DO_CHAT?format=skill","oip_contract":"/api/dispatch?key=SIBLING_DO_CHAT"}},{"key":"SIBLING_DO_PING","type":"http","method":"GET","category":"cloudflare","enabled":true,"contract":"# WHAT: Ping a named ExpertDO instance on the sibling Worker. Each name gets its own Durable Object id, its own SQLite state. $1=DO name (e.g. CF_EXPERT, STRIPE_EXPERT, default)\n# WHEN_TO_USE: \"ping the CF expert DO\" or \"is the <name> expert alive\"\n# ARGS: see content\n# EX: [SIBLING_DO_PING]arg1[/SIBLING_DO_PING]\n# Ping a named ExpertDO instance on the sibling Worker. Each name gets its own Durable Object id, its own SQLite state. $1=DO name (e.g. CF_EXPERT, STRIPE_EXPERT, default).\n# WHEN_TO_USE: \"ping the CF expert DO\" or \"is the <name> expert alive\"","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/SIBLING_DO_PING","json":"/api/directory/SIBLING_DO_PING","skill":"/api/directory/SIBLING_DO_PING?format=skill","oip_contract":"/api/dispatch?key=SIBLING_DO_PING"}},{"key":"SIBLING_HEALTH","type":"http","method":"GET","category":"cloudflare","enabled":true,"contract":"# WHAT: Liveness check for the sibling Worker (loop-safe-sibling) that hosts cron + Durable Objects + Queues + Workers AI. Returns {ok,name,ts}. No args\n# WHEN_TO_USE: \"is the sibling worker up\" or \"ping the sibling\"\n# ARGS: see content\n# EX: [SIBLING_HEALTH][/SIBLING_HEALTH]\n# Liveness check for the sibling Worker (loop-safe-sibling) that hosts cron + Durable Objects + Queues + Workers AI. Returns {ok,name,ts}. No args.\n# WHEN_TO_USE: \"is the sibling worker up\" or \"ping the sibling\"","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/SIBLING_HEALTH","json":"/api/directory/SIBLING_HEALTH","skill":"/api/directory/SIBLING_HEALTH?format=skill","oip_contract":"/api/dispatch?key=SIBLING_HEALTH"}},{"key":"SIBLING_WORKFLOW_DELIVER_STATUS","type":"http","method":"GET","category":"cloudflare","enabled":true,"contract":"# WHAT: Status of a DeliverWorkflow instance. $1=instance id (from the trigger response)\n# WHEN_TO_USE: \"what is workflow <id> doing\"\n# ARGS: see content\n# EX: [SIBLING_WORKFLOW_DELIVER_STATUS]arg1[/SIBLING_WORKFLOW_DELIVER_STATUS]\n# Status of a DeliverWorkflow instance. $1=instance id (from the trigger response).\n# WHEN_TO_USE: \"what is workflow <id> doing\"","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/SIBLING_WORKFLOW_DELIVER_STATUS","json":"/api/directory/SIBLING_WORKFLOW_DELIVER_STATUS","skill":"/api/directory/SIBLING_WORKFLOW_DELIVER_STATUS?format=skill","oip_contract":"/api/dispatch?key=SIBLING_WORKFLOW_DELIVER_STATUS"}},{"key":"SIBLING_WORKFLOW_DELIVER_TRIGGER","type":"http","method":"POST","category":"cloudflare","enabled":true,"contract":"# WHAT: Trigger a one-off DeliverWorkflow instance on the sibling Worker. Returns {id, status}. $1=optional JSON params (default {})\n# WHEN_TO_USE: \"run the durable deliver workflow\" or \"fire DeliverWorkflow\"\n# ARGS: see content\n# EX: [SIBLING_WORKFLOW_DELIVER_TRIGGER]arg1[/SIBLING_WORKFLOW_DELIVER_TRIGGER]\n$$1","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/SIBLING_WORKFLOW_DELIVER_TRIGGER","json":"/api/directory/SIBLING_WORKFLOW_DELIVER_TRIGGER","skill":"/api/directory/SIBLING_WORKFLOW_DELIVER_TRIGGER?format=skill","oip_contract":"/api/dispatch?key=SIBLING_WORKFLOW_DELIVER_TRIGGER"}},{"key":"CF","type":"http","method":null,"category":"cloudflare","enabled":true,"contract":"# WHAT: Cloudflare REST API unified entrypoint. 256+ operations.\n# WHEN_TO_USE: any Cloudflare API call (KV, D1, R2, Workers, DNS, etc.).\n# ARGS: operation|account_id|... (first arg selects the sub-operation from the target_map).\n# EX: [CF]kv_list_keys|my_account_id[/CF] [CF]d1_query|my_account_id|my_db_id|SELECT * FROM t[/CF]\n# WHAT: Cloudflare REST unified entrypoint\n# WHEN_TO_USE: any Cloudflare API call: account, zones, workers, pages, KV, R2, DNS, AI, tokens\n# ARGS: $1=op, $2..$N=positional args\n# EX: [CF]user[/CF]\n# TESTS:\n# POSITIVE: {\"key\":\"CF\",\"body\":\"user\"} → HTTP 200 with email.\n# INVERSE: {\"key\":\"CF\",\"body\":\"xxx\"} → starts with ERR:target_map:unknown_op\n","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/CF","json":"/api/directory/CF","skill":"/api/directory/CF?format=skill","oip_contract":"/api/dispatch?key=CF"}},{"key":"DURABLE_WORKER","type":"http","method":"GET","category":"cloudflare","enabled":true,"contract":"# WHAT: Durable Worker — the bound Durable Object (class DirectoryDO, script loop-safe-directory-do). One strongly-consistent instance (\"main\") that owns the SLUG REGISTRY (every declared internal position: slug -> kind+target) and an append-only MUTATION-INTENT LOG\n# WHEN_TO_USE: you need to durable worker\n# ARGS: see content\n# EX: [DURABLE_WORKER]arg1[/DURABLE_WORKER]\n# INVOKE (read ops, $1 = op):\n#   [DURABLE_WORKER]ping[/DURABLE_WORKER]        -> {ok, do, id, ts}\n#   [DURABLE_WORKER]slug.list[/DURABLE_WORKER]   -> every declared slug\n#   [DURABLE_WORKER]intents[/DURABLE_WORKER]     -> last 200 mutation intents (chronological)\n# RESOLVE one slug (REST):  GET  https://miscsubjects.com/api/durable/slug.resolve?slug=<slug>\n# REGISTER a slug (REST):   POST https://miscsubjects.com/api/durable/slug.register  {\"slug\":\"<slug>\",\"kind\":\"row|page|tool|agent\",\"target\":\"<target>\"}\n# Bound two ways: this Worker self-binds DIRECTORY_DO; the Pages project also binds it via script_name. Deploy the Worker before the Pages deploy.\n{\"op\":\"$1\"}","input_schema":null,"examples":null,"authority_required":true,"representations":{"article":"/a/directory/DURABLE_WORKER","json":"/api/directory/DURABLE_WORKER","skill":"/api/directory/DURABLE_WORKER?format=skill","oip_contract":"/api/dispatch?key=DURABLE_WORKER"}},{"key":"TOOLING_DOCS","type":"http","method":"GET","category":"cloudflare","enabled":true,"contract":"# WHAT: Platform + protocol references (external)\n# WHEN_TO_USE: you need to tooling docs\n# ARGS: see content\n# EX: [TOOLING_DOCS][/TOOLING_DOCS]\n# Platform + protocol references (external).\n# Cloudflare   https://developers.cloudflare.com · api https://api.cloudflare.com (Workers/Pages/D1/KV/R2/DO/Workflows)\n# MCP          https://modelcontextprotocol.io\n# JSON Schema  https://json-schema.org\n# MDN          https://developer.mozilla.org\n# GitHub repo  https://github.com/massoumicyrus/miscsubjects-pages · api https://api.github.com","input_schema":null,"examples":null,"authority_required":false,"representations":{"article":"/a/directory/TOOLING_DOCS","json":"/api/directory/TOOLING_DOCS","skill":"/api/directory/TOOLING_DOCS?format=skill","oip_contract":"/api/dispatch?key=TOOLING_DOCS"}}]},"ontology":{"conformance_group":"article","inferred_from":["cloudflare","architecture","security","cloudflare-os","access","zero-trust","authentication","cloudflare","os","access"],"relationships":[],"sources":[]},"conformance":{"success_events":"/api/articles/cloudflare-os-access/invocations?status=success","failure_events":"/api/articles/cloudflare-os-access/invocations?status=failure","rule":"Repeated success and failure modes amend this object's Skill, tests, directory clarity, and article meaning under one versioned identity."},"article":{"slug":"cloudflare-os-access","title":"Cloudflare Access authenticates the edge, not your application","body":"Cloudflare Access sits between a request and an origin. For a person, it turns an application URL into an identity check: Cloudflare redirects the browser to an identity provider, applies an Access policy, and issues a signed session token. For a machine, there is no login page. It must send a service credential on the first request, and the Access policy must explicitly accept that credential.\n\nThe distinction that decides the design:\n\n> Access proves that a request satisfied an edge policy. Your application still decides what that authenticated principal may do.\n\nA service token can pass Access and still carry no human identity. A Bypass rule can make a path reachable while removing Access authentication and Access logging from that path. Deleting the application does not prove the service token was deleted, and deleting the token does not prove the application or policy disappeared. Those are separate objects with separate list and delete operations.\n\n## Evidence status\n\n**Observed** marks first-party measurements or runtime receipts from the named environment.\n**Derived** marks arithmetic calculated from cited inputs. **Specified** marks vendor or standards\ndocumentation. **Implemented** and **deployed** name code and live-state evidence, respectively.\n**Reproduced** means the stated procedure was rerun. **Externally attested** marks operator reports;\nthose reports show that an experience occurred, not that it is universal.\n\n## The request path, without product names hiding the mechanics\n\n| Stage | Human request | Machine request |\n| --- | --- | --- |\n| 1. Request arrives | Browser requests the protected hostname and path | HTTP client requests the same URL |\n| 2. Access checks credential | Looks for a valid `CF_Authorization` cookie | Looks for service-token headers or another non-human credential |\n| 3. No valid credential | Redirects to the Access login flow | Usually a `302` the client cannot use, or `401`/`403` when Service Auth handling is configured |\n| 4. Policy evaluation | Allow, Block, Bypass, or a more specific rule | Service Auth, mTLS, or Bypass |\n| 5. Origin request | Cloudflare forwards `Cf-Access-Jwt-Assertion` | Cloudflare forwards an application JWT after service authentication |\n| 6. Origin authorization | Verify signature, issuer and audience; map `email` or `sub` to an app role | Verify the same fields; map `common_name` to a synthetic machine principal |\n\nAccess is not an origin firewall. Unless the origin is connected only through Cloudflare Tunnel or otherwise restricted to Cloudflare, an attacker may try to reach it directly and avoid the Access layer. Even when every request must pass Cloudflare, the origin still verifies the JWT. Cloudflare's application-token reference is blunt: validation of the header alone is insufficient because an unverified header can be spoofed.\n\n## Create one self-hosted application in the dashboard\n\nPrerequisites: a Cloudflare account, a Zero Trust organization, a domain on Cloudflare, and an identity provider. The built-in one-time PIN flow is enough for a small first deployment; an organization using group rules should connect its existing SAML or OIDC provider and confirm the exact group claim before writing policy.\n\nCurrent dashboard path:\n\n1. Open **Zero Trust**.\n2. Go to **Access controls** → **Applications**.\n3. Select **Add an application**.\n4. Choose **Self-hosted**.\n5. Set **Application name**.\n6. Under **Session Duration**, choose how long the application JWT remains valid.\n7. Under **Add public hostname**, enter **Subdomain**, **Domain**, and optional **Path**. A path makes the Access application narrower than the hostname.\n8. Under **Access policies**, create or attach a policy.\n9. Choose the identity providers shown on the login page.\n10. Save, then test one allowed identity and one denied identity before widening the selectors.\n\nAccess applications are deny-by-default. Creating the hostname without an Allow or Service Auth policy does not grant anyone access.\n\nThe four policy actions do different jobs:\n\n| Action | What a match means | Correct use | Dangerous misunderstanding |\n| --- | --- | --- | --- |\n| **Allow** | The request may continue after identity authentication | People selected by email, IdP group, country, device posture, or another identity rule | “Not blocked” does not mean allowed; unmatched users remain denied |\n| **Block** | The matching request is denied | Carve a narrow denial out of a broader Allow rule | A Block rule alone does not make everyone else allowed |\n| **Bypass** | Access enforcement is disabled for the matching traffic | A deliberately public webhook or health path whose own exposure is accepted | No Access identity, controls, or Access logs remain on that path |\n| **Service Auth** | A non-IdP credential may pass | Service tokens or mutual TLS for automation | A service token is not a human and may not have `sub` or `email` |\n\nPolicy order and selectors matter. Test with the policy tester, then make real HTTP requests. A green dashboard object is configuration evidence, not traffic evidence.\n\n## The same application and policy through the REST API\n\nUse a Cloudflare API token scoped to **Access: Apps and Policies Write**. Keep the account id and API token in environment variables; neither belongs in shell history, an article, or a CI log.\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\n    \"name\": \"admin surface\",\n    \"type\": \"self_hosted\",\n    \"domain\": \"admin.example.com\",\n    \"session_duration\": \"8h\",\n    \"app_launcher_visible\": false,\n    \"service_auth_401_redirect\": true\n  }'\n```\n\nCapture `result.id` as `ACCESS_APP_ID`. Do not hand-type it.\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$ACCESS_APP_ID/policies\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\n    \"name\": \"named administrators\",\n    \"decision\": \"allow\",\n    \"precedence\": 1,\n    \"include\": [\n      {\"email_domain\": {\"domain\": \"example.com\"}}\n    ]\n  }'\n```\n\nThe API response must say `success: true`. Follow it with a fresh GET of the exact application. A `201` proves creation, but the GET proves the stored hostname, policy and session settings are the ones you intended.\n\nFor infrastructure automation, create a service token separately:\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/service_tokens\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\"name\":\"deploy smoke\",\"duration\":\"720h\"}'\n```\n\nThe client secret is returned once. Store it in the deployment secret store immediately. The token still does nothing until a policy accepts it:\n\n```sh\ncurl -sS -X POST \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$ACCESS_APP_ID/policies\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\" \\\n  -H \"content-type: application/json\" \\\n  --data '{\n    \"name\": \"deployment machine\",\n    \"decision\": \"non_identity\",\n    \"precedence\": 2,\n    \"include\": [{\"any_valid_service_token\": {}}]\n  }'\n```\n\nCloudflare's API calls the `non_identity` decision **Service Auth** in the dashboard. That naming difference is worth writing in the runbook; otherwise an operator comparing JSON with the UI can think the wrong policy was created.\n\n## A service token is two secret headers and one policy\n\nThe normal first request carries:\n\n```sh\ncurl -sS https://admin.example.com/health \\\n  -H \"CF-Access-Client-Id: $ACCESS_CLIENT_ID\" \\\n  -H \"CF-Access-Client-Secret: $ACCESS_CLIENT_SECRET\"\n```\n\nCloudflare checks the two values, evaluates a Service Auth policy, and forwards the request with `Cf-Access-Jwt-Assertion`. A successful request can also return a `CF_Authorization` cookie. If the application contains only Service Auth policies, Cloudflare requires the service token on subsequent requests too; the JWT cookie alone is not enough.\n\nAccess also supports a single custom header containing both values. That helps SaaS clients with one configurable authorization field. It does not help software with no custom-header extension point.\n\nThat limitation is common, not theoretical.\n\n`kennypy` put Jellyfin behind Access. Google SSO worked in a browser, but the Findroid client could not add the two headers and became LAN-only. `hippiuS` hit the same shape with an MCP client calling ArgoCD: the request became a `302` to an SSO page a non-browser could not follow, or a `403`. The ArgoCD CLI needed a general `--header` flag before it could work with this class of proxy authentication.\n\nThe rule: **check the client's HTTP surface before choosing Access for the endpoint.** “It can call HTTPS” is insufficient. It must be able to set two headers, one configured compound header, mTLS credentials, or an Access-aware token.\n\n## The service-token JWT has authority but may have no user\n\nThe recovered first-party probe created a temporary self-hosted application, added Service Auth, minted a temporary service token, and called a protected path. With valid headers, Access let the request reach the origin. The origin returned its own `404`, which is the useful proof: the credential cleared the edge policy.\n\nThe redacted application-token payload had this shape:\n\n```json\n{\n  \"type\": \"app\",\n  \"iat\": 1785041363,\n  \"exp\": 1785043164,\n  \"iss\": \"https://<team-name>.cloudflareaccess.com\",\n  \"sub\": \"\",\n  \"aud\": [\"<application-audience>\"],\n  \"common_name\": \"<service-token-client-id>\"\n}\n```\n\nThere was no `email` claim. `sub` was the empty string.\n\n`dataGriff` documented the consequence in a CI smoke test: once Access was enforced, the service-token caller had no user id to own a review and no human admin standing. The correct repair is not to invent an email inside every handler. Map the verified service principal once, at the authentication seam:\n\n```js\nfunction principalFromAccessClaims(claims) {\n  if (claims.type === \"app\" && claims.common_name) {\n    return {\n      kind: \"machine\",\n      id: `access-service:${claims.common_name}`,\n      roles: [\"deploy-smoke\"],\n    };\n  }\n  if (claims.email && claims.sub) {\n    return {\n      kind: \"human\",\n      id: claims.sub,\n      email: claims.email,\n      roles: rolesForEmail(claims.email),\n    };\n  }\n  throw new Error(\"Access token has no usable principal\");\n}\n```\n\nThe application authorizes `deploy-smoke` to do only the smoke-test operations. It does not promote every service token to administrator. `common_name` is useful only after the JWT signature, issuer and audience have passed.\n\n## Verify the JWT at the origin\n\nRead `Cf-Access-Jwt-Assertion`. Cloudflare recommends that header because the cookie is not guaranteed to reach the origin. Then verify:\n\n1. The signature against the team's JWKS.\n2. `alg` is the expected RS256 algorithm.\n3. `iss` equals the exact team-domain issuer.\n4. `aud` contains the exact Access application audience tag.\n5. `exp` and `nbf` permit the current time.\n6. The resulting human or machine principal is authorized for this application action.\n\nWith `jose`:\n\n```js\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\n\nconst TEAM_DOMAIN = process.env.ACCESS_TEAM_DOMAIN;\nconst ACCESS_AUD = process.env.ACCESS_AUD;\nconst issuer = `https://${TEAM_DOMAIN}`;\nconst jwks = createRemoteJWKSet(\n  new URL(`${issuer}/cdn-cgi/access/certs`),\n);\n\nexport async function requireAccess(request) {\n  const token = request.headers.get(\"Cf-Access-Jwt-Assertion\");\n  if (!token) return { ok: false, status: 401, error: \"missing Access JWT\" };\n\n  try {\n    const { payload, protectedHeader } = await jwtVerify(token, jwks, {\n      issuer,\n      audience: ACCESS_AUD,\n      algorithms: [\"RS256\"],\n    });\n    return {\n      ok: true,\n      claims: payload,\n      algorithm: protectedHeader.alg,\n      principal: principalFromAccessClaims(payload),\n    };\n  } catch {\n    return { ok: false, status: 403, error: \"invalid Access JWT\" };\n  }\n}\n```\n\nDo not hard-code a PEM. The public endpoint carries the current signing key and the previous rotated key. The fresh read on this account returned two RSA signing keys, both RS256. A remote JWKS loader selects by `kid` and survives rotation.\n\n## Bypass is a public route, not machine authentication\n\nA Bypass policy removes Access from matching traffic. Cloudflare does not apply Access security controls to it, and the request is not present in Access logs. That can be correct for a public payment webhook whose provider cannot send Access credentials, provided the handler verifies the provider's own signature and rejects replay.\n\nIt is not a shortcut for a private API.\n\n`lesbass` reported a split application where the unauthenticated health endpoint worked while every company-scoped API call failed with `RESPONSIBLE_USER_UNAVAILABLE`. The Access identity existed, but it did not map to a company member. Opening more paths would hide the identity defect by removing authentication from them.\n\nUse the narrowest path possible. Put a separate handler-level signature on a bypassed webhook. Do not Bypass `/api/*` because one vendor callback needs to be public.\n\n## Deleting one object proves nothing about the other two\n\nAn Access deployment usually creates at least three resources:\n\n| Resource | What deleting it removes | What remains |\n| --- | --- | --- |\n| Access application | Hostname/path protection and attached application policies | Reusable policies and service tokens may remain |\n| Application policy | One Allow, Block, Bypass or Service Auth decision | Application and credentials remain |\n| Service token | That client id and secret | Application and Service Auth policy remain, ready to accept another valid token |\n\nThe proof sequence is explicit:\n\n```sh\ncurl -sS -X DELETE \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$ACCESS_APP_ID\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\ncurl -sS -X DELETE \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/service_tokens/$SERVICE_TOKEN_ID\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\ncurl -sS \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n\ncurl -sS \\\n  \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/service_tokens\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_API_TOKEN\"\n```\n\nRequire HTTP success and Cloudflare `success: true` before examining either list. A `401`, `403`, or empty parse is not absence proof. The final lists must contain neither the exact id nor the exact temporary name.\n\nThe temporary measurement used for this page was rechecked at `2026-07-26T06:09:10Z`. Both authenticated lists succeeded. Neither the application id/name nor the service-token id/name was present. Zero probe resources remained.\n\n## When a single bearer key is the better answer\n\nThis application's owner surface does not use Access. Its middleware accepts one owner key by request header or URL parameter, or a signed 60-day HttpOnly admin-session cookie minted after the key is entered once. An unauthenticated browser is redirected to the public login page. An unauthenticated machine request receives a bounded `401` JSON object.\n\nThat posture accepts a sharp trade: one strong secret has no person-level identity, no IdP offboarding and no device-posture check. In return, any HTTP client that can set one header can use it, the application controls the exact failure response, and machine calls do not depend on an SSO redirect.\n\nFor one owner and a closed automation surface, that can beat Access. For 20 administrators who need individual revocation and audit attribution, it does not.\n\n| Option | Best fit | Identity | Machine-client requirement | Verdict |\n| --- | --- | --- | --- | --- |\n| Cloudflare Access | Several people, existing IdP, per-person revocation | Human email/groups; machine principal for service tokens | Custom headers, mTLS, or Access-aware client | Default for a shared admin UI |\n| mTLS | Services or managed devices with certificate lifecycle | Certificate subject or mapped device | Client-certificate support | Strong machine auth; heavier issuance and rotation |\n| One bearer key checked in the Worker | One owner, small fixed set of scripts | Shared principal only | One configurable header | Best simple answer when per-person identity adds no value |\n| IP allow-list | Fixed corporate egress as one factor | Network location, not a person | Stable source IP | Use as a condition, not the only credential |\n| Tunnel plus Access | Private origin that must not be directly reachable | Access identity plus private origin path | Browser login or service credential | Strongest Access topology for a self-hosted origin |\n| Bypass plus handler signature | One third-party webhook | Provider key/signature | Provider-specific signed request | Correct for that path only |\n\n## Seats and arithmetic\n\nCloudflare's current plan page says the Free plan is for teams under 50 users and costs $0. Pay-as-you-go is $7 per user per month. The page describes Remote Browser Isolation as an add-on but does not publish its current add-on price. A dated 2023 operator comparison recorded $10 per user per month; treat that as historical evidence, not today's quote.\n\n| Administrators | Access Free | Pay-as-you-go at $7/seat/month | Shared bearer key |\n| ---: | ---: | ---: | ---: |\n| 1 | $0 | $7 | $0 product fee |\n| 12 | $0 | $84 | $0 product fee |\n| 49 | $0 | $343 | $0 product fee |\n| 60 | Plan choice required; outside “under 50” positioning | $420 | $0 product fee, but 60 people sharing one key is indefensible |\n| 250 | Not the free-plan fit | $1,750 | Wrong architecture |\n\nThe calculation is seats × $7. It excludes support, identity-provider cost, implementation time, and any separately quoted Remote Browser Isolation add-on. A bearer key has no Cloudflare seat line item, but secret rotation and the absence of individual attribution are costs; they are just paid in operator time and incident risk.\n\n## What administering Access feels like\n\nThe policy surface is capable. The console has drawn specific criticism. `systemvoltage`, otherwise positive about Cloudflare's main dashboard, described the Access/Zero Trust area as a separate application that took ten seconds and redirected repeatedly, with worse UI and thin documentation.\n\nThat report is dated 2022. Do not turn it into a claim about today's page speed. Keep the durable operational lesson: the person on call needs the API paths and curl proofs in the runbook, because a graphical console can be slow, moved, or unavailable.\n\nThe positive operator case is equally concrete. `tbhb` uses Tunnel plus Access to expose only the local-development endpoints that must be public, such as webhooks, while keeping the rest of the site behind Access. That is the product boundary working: narrow public ingress, authenticated private remainder, and no directly published origin.\n\n## Error, cause, repair\n\n| Symptom | Cause | Repair |\n| --- | --- | --- |\n| `302` to `*.cloudflareaccess.com/cdn-cgi/access/login/...` | No accepted credential and the application is using interactive login behavior | Browser: complete the IdP flow. Machine: send a service token and add Service Auth, or enable the documented 401 response for Service Auth |\n| `403` before the origin | Invalid service headers, no matching policy, wrong application path, or denied selector | Confirm both header names, list the application and policies, then test the exact hostname/path |\n| Valid service token reaches origin but `sub` is empty | Service-token application JWT is non-human | Map verified `common_name` to a least-privilege synthetic machine identity |\n| JWT signature verification fails | Wrong issuer, wrong audience, stale hard-coded key, altered token, or wrong algorithm | Fetch the team JWKS, select by `kid`, require RS256, exact issuer and exact application audience |\n| Browser works; native client gets 302/403 | Client cannot add Access service-token headers | Add a general custom-header option, use the single-header mode, mTLS, or do not put that endpoint behind Access |\n| Health works; every scoped API call fails | Health is public/Bypass while authenticated principal is not mapped into app membership | Fix identity mapping at the auth seam; do not widen Bypass |\n| Origin accepts a claimed Access header without cryptographic verification | Application trusts attacker-supplied text | Verify JWT signature, issuer, audience and time before reading identity |\n| Temporary app appears deleted but token remains | Only the Access application was deleted | Delete the service token separately; require fresh successful lists for both collections |\n\n## Three live receipts, with bounded claims\n\n**Access path.** A temporary application protected a unique path. Plain HTTP returned `302` before Service Auth and `403` after the Service Auth policy existed. Correct service-token headers passed Access and reached the origin, which returned its own `404`. The redacted JWT used RS256, had `type: \"app\"`, an audience and `common_name`, an empty `sub`, and no `email`.\n\n**Signing keys.** A fresh unauthenticated GET of the account's team JWKS returned HTTP `200`, 4,914 JSON bytes and two RSA/RS256 signing keys. The published command uses a placeholder, not the real team name:\n\n```sh\ncurl -sS \"https://<team-name>.cloudflareaccess.com/cdn-cgi/access/certs\" \\\n  | jq '{keys: [.keys[] | {kid, alg, kty, use}]}'\n```\n\n**This application's key-only admin gate.** A fresh machine request with no credential:\n\n```sh\ncurl -sS -D - https://miscsubjects.com/admin \\\n  -H 'accept: application/json'\n```\n\nreturned HTTP `401`, `application/json`, and a 47-byte body with only `error` and `login`. A scan found no stack, trace, binding, database, exception or key marker. This proves the unauthenticated failure is bounded; it does not prove the shared-key posture has person-level identity.\n\n**Cleanup.** Successful authenticated Access application and service-token lists proved the temporary ids and names absent. No `401`, `403`, or failed parse was treated as an empty list.\n\nAccess earns its complexity when identity changes the authorization decision. If every accepted caller is the same owner and every client already holds the same operational secret, one checked key is smaller and often more reliable. Once individual revocation, IdP groups or device posture matter, use Access, verify the JWT at the origin, and give machines a principal of their own.\n\nThis chapter is part of [the Cloudflare account inventory](/a/cloudflare-os). For the private-origin and Durable Object boundary, see [Workers and Durable Objects](/a/cloudflare-os-workers).\n","hero":"https://miscsubjects.com/img/up/cloudflare-os-access-hero-card.png","images":[{"url":"https://miscsubjects.com/img/up/cloudflare-os-access-hero-card.png","alt":"Black-and-white Cloudflare OS Access title card stating that policy is not an application key","caption":"Cloudflare OS · Access · the identity gate"}],"style":{},"tags":["cloudflare","architecture","security","cloudflare-os","access","zero-trust","authentication"],"model":"GPT-5.6 (Codex)","ledger":{"href":"/api/articles/cloudflare-os-access/ledger","live":true},"embeds":[],"widgets":[{"type":"stat","value":"302 → 403 → 404","label":"unauthenticated, Service Auth policy, then valid service token reaching origin"},{"type":"stat","value":"2","label":"RSA/RS256 signing keys returned by the fresh team JWKS read"},{"type":"stat","value":"0","label":"temporary Access applications and service tokens left after successful list checks"},{"type":"stat","value":"401 / 47 B","label":"bounded unauthenticated machine response from this application's admin gate"},{"type":"stat","value":"$0","label":"advertised Access Free plan for teams under 50"},{"type":"stat","value":"$7","label":"current pay-as-you-go price per user per month"},{"type":"quote","quote":"Access proves that a request satisfied an edge policy. Your application still decides what that authenticated principal may do.","by":"Operator thesis"},{"type":"note","title":"Deletion proof","body":"Application, application policy, and service token are separate objects. Verify each collection with a fresh successful read."}],"home":true,"claims":[{"id":"c1","text":"Access authenticates a request against an edge policy; the origin application still owns authorization.","section":"Thesis","tier":"system","source_ids":["s1","s2","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c2","text":"A browser session presents CF_Authorization while an origin should verify Cf-Access-Jwt-Assertion cryptographically.","section":"Request path","tier":"mechanism","source_ids":["s1","s2","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c3","text":"Self-hosted Access applications deny by default until an Allow or Service Auth policy matches.","section":"Application","tier":"fact","source_ids":["s6"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c4","text":"Bypass disables Access enforcement and removes matching traffic from Access logs.","section":"Policies","tier":"fact","source_ids":["s4"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c5","text":"Service Auth is the policy action for service tokens and other non-IdP authentication such as mTLS.","section":"Policies","tier":"fact","source_ids":["s4","s5"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c6","text":"The Access REST API creates applications and service tokens as separate resources, and a new service-token secret is shown only once.","section":"REST API","tier":"mechanism","source_ids":["s7","s8"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c7","text":"A normal service-token request needs both CF-Access-Client-Id and CF-Access-Client-Secret plus a matching Service Auth policy.","section":"Service tokens","tier":"mechanism","source_ids":["s5"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c8","text":"Some native clients cannot use Access because they cannot add its custom authentication headers.","section":"Client compatibility","tier":"people","source_ids":["p1","p2"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"externally attested"},{"id":"c9","text":"A service-token application JWT may carry an empty sub and no email, so the application must map a verified machine principal explicitly.","section":"Machine identity","tier":"system","source_ids":["p3","r2"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c10","text":"The example origin verifier uses jose to enforce issuer, audience and RS256 against a remote JWKS.","section":"JWT verification","tier":"implementation","source_ids":["s12","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified + implemented"},{"id":"c11","text":"The measured team JWKS exposed two RSA/RS256 signing keys, matching the current-and-previous rotation model.","section":"JWT verification","tier":"runtime","source_ids":["r3","s3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed + specified"},{"id":"c12","text":"An open health path does not prove authenticated application membership; an authenticated principal can still fail scoped authorization.","section":"Authorization seam","tier":"people","source_ids":["p4"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"externally attested"},{"id":"c13","text":"Deleting an Access application, its policy, and a reusable service token are distinct lifecycle operations.","section":"Deletion proof","tier":"system","source_ids":["r4","s7","s8"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed + specified"},{"id":"c14","text":"Successful fresh lists must prove both the temporary application and service token absent by exact id and name.","section":"Deletion proof","tier":"runtime","source_ids":["r4"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c15","text":"For one owner and closed automation, one application-checked bearer key can be smaller than Access; it gives up person-level identity and IdP offboarding.","section":"Alternatives","tier":"system","source_ids":["r3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c16","text":"Cloudflare currently advertises Free for teams under 50 and pay-as-you-go at seven dollars per user per month.","section":"Pricing","tier":"fact","source_ids":["s9"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified"},{"id":"c17","text":"Remote Browser Isolation is currently an add-on, while the ten-dollar figure is a dated 2023 operator observation rather than today's vendor quote.","section":"Pricing","tier":"fact","source_ids":["p6","s10"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified + externally attested"},{"id":"c18","text":"A dated operator report criticized the Access console's speed and redirects; it is historical usability evidence, not a current benchmark.","section":"Operations","tier":"people","source_ids":["p5"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"externally attested"},{"id":"c19","text":"Tunnel plus Access can keep an origin private while a narrow webhook path remains deliberately reachable.","section":"Topology","tier":"system","source_ids":["p7","s11"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"specified + externally attested"},{"id":"c20","text":"The temporary probe moved from 302 to 403 when Service Auth was attached, and a valid service token then cleared Access and reached the origin's 404.","section":"Live proof","tier":"runtime","source_ids":["r1"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"},{"id":"c21","text":"The application's unauthenticated machine path returns bounded JSON: HTTP 401, 47 bytes, and only error and login keys.","section":"Live proof","tier":"runtime","source_ids":["r3"],"why_material":"Supports the article's operator decision or verification path.","evidence_status":"observed"}],"sources":[{"id":"s1","type":"specification","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/","title":"Authorization cookie","publisher":"Cloudflare","quote":"When you protect a site with Cloudflare Access, Cloudflare checks every HTTP request bound for that site to ensure that the request has a valid `CF_Authorization` cookie.","summary":"Specifies the browser session cookie checked on protected HTTP requests.","claim_ids":["c1","c2"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"genesis","hash":"4772a7e4fab4e24aa6a452a69cac536c0d97bdbf0d3c7e563d7f3ac63520aefa"},{"id":"s2","type":"specification","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/application-token/","title":"Application token","publisher":"Cloudflare","quote":"Validation of the header alone is not sufficient — the JWT and signature must be confirmed to avoid identity spoofing.","summary":"Defines the signed application token forwarded to an origin and warns against trusting an unverified header.","claim_ids":["c1","c2"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"4772a7e4fab4e24aa6a452a69cac536c0d97bdbf0d3c7e563d7f3ac63520aefa","hash":"f9c66f5527e8d5372e10d980921fddb7d25d438d46eea8292d8c2138f0262056"},{"id":"s3","type":"specification","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/","title":"Validate JWTs","publisher":"Cloudflare","quote":"We recommend validating the `Cf-Access-Jwt-Assertion` header instead of the `CF_Authorization` cookie, since the cookie is not guaranteed to be passed.","summary":"Documents origin JWT validation and the two-key rotation behavior of the team JWKS.","claim_ids":["c1","c10","c11","c2"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"f9c66f5527e8d5372e10d980921fddb7d25d438d46eea8292d8c2138f0262056","hash":"0203131b199a02c6e39830fe36dd1cbd9057c86a3f71e338568f663c62d14349"},{"id":"s4","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/policies/","title":"Access policies","publisher":"Cloudflare","quote":"The Bypass action in Cloudflare Access disables Access enforcement for specific traffic.","summary":"Defines Allow, Block, Bypass and Service Auth behavior, including the loss of Access logs on bypassed traffic.","claim_ids":["c4","c5"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"0203131b199a02c6e39830fe36dd1cbd9057c86a3f71e338568f663c62d14349","hash":"49ba1d224dd549423687fdbd50f6bee94ca2431896f043e2e9245f59ef55eb04"},{"id":"s5","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/","title":"Service tokens","publisher":"Cloudflare","quote":"Make sure to set the policy action to Service Auth; otherwise, Access will prompt for an identity provider login.","summary":"Explains service-token creation, the two default headers, single-header mode and Service Auth policy requirement.","claim_ids":["c5","c7"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"49ba1d224dd549423687fdbd50f6bee94ca2431896f043e2e9245f59ef55eb04","hash":"baa2144b975ef35a40c9228f989d2ec997eead832277982c8f08675b9bf11005"},{"id":"s6","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app/","title":"Add a self-hosted application","publisher":"Cloudflare","quote":"Access applications are deny by default.","summary":"Gives the dashboard creation sequence and default-deny posture for self-hosted applications.","claim_ids":["c3"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"baa2144b975ef35a40c9228f989d2ec997eead832277982c8f08675b9bf11005","hash":"cf61155f6c68dffe4621d724256d99f619aaaf598429f0461a8c87066d623e61"},{"id":"s7","type":"publisher_documentation","url":"https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/applications/methods/create/","title":"Create an Access application","publisher":"Cloudflare API","quote":"Adds a new application to Access.","summary":"Primary REST reference for creating an Access application.","claim_ids":["c13","c6"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"cf61155f6c68dffe4621d724256d99f619aaaf598429f0461a8c87066d623e61","hash":"abf2eea94b155e2279504d0db7e63d4bdb9d52619a47dd555b5c154da691d821"},{"id":"s8","type":"publisher_documentation","url":"https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/service_tokens/methods/create/","title":"Create a service token","publisher":"Cloudflare API","quote":"This is the only time you can get the Client Secret. If you lose the Client Secret, you will have to create a new Service Token.","summary":"Primary REST reference for service-token creation and its one-time secret.","claim_ids":["c13","c6"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"abf2eea94b155e2279504d0db7e63d4bdb9d52619a47dd555b5c154da691d821","hash":"f18f12824a21b0068e0a16b9b57378d60c7a568ac38ea6cf57afd825f9bf9d7b"},{"id":"s9","type":"publisher_documentation","url":"https://www.cloudflare.com/plans/zero-trust-services/","title":"Zero Trust services plans","publisher":"Cloudflare","quote":"$7 user / month","summary":"Current plan page listing Free for teams under 50 and pay-as-you-go at seven dollars per user per month.","claim_ids":["c16"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"f18f12824a21b0068e0a16b9b57378d60c7a568ac38ea6cf57afd825f9bf9d7b","hash":"3f91577a338eff7d436144d74712908d49f0ec34f6f96c3dcf395085a1b4d8e2"},{"id":"s10","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/remote-browser-isolation/","title":"Remote Browser Isolation","publisher":"Cloudflare","quote":"Cloudflare Browser Isolation is available as an add-on for Cloudflare One plans.","summary":"Current documentation classifies browser isolation as an add-on without supplying the historical ten-dollar figure used by an operator in 2023.","claim_ids":["c17"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"3f91577a338eff7d436144d74712908d49f0ec34f6f96c3dcf395085a1b4d8e2","hash":"8bd679d7a822598b84c0c199113cfb07ab68f9ef0ab81e336ec5c5e657d67c2a"},{"id":"s11","type":"publisher_documentation","url":"https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/","title":"Cloudflare Tunnel","publisher":"Cloudflare","quote":"Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address.","summary":"Defines the private-origin side of the Tunnel-plus-Access pattern.","claim_ids":["c19"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"8bd679d7a822598b84c0c199113cfb07ab68f9ef0ab81e336ec5c5e657d67c2a","hash":"19b3957680495679c4ddc81d9b095aa0b53a224d33ccb28e9b6c7bffae692dd9"},{"id":"s12","type":"repository","url":"https://github.com/panva/jose","title":"panva/jose","publisher":"GitHub","author":"Filip Skokan and contributors","quote":"`jose` is JavaScript module for JSON Object Signing and Encryption, providing support for JSON Web Tokens (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), and JSON Web Key Set (JWKS).","summary":"Repository for the standards-based JWT/JWKS library used in the origin-verification example.","claim_ids":["c10"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"19b3957680495679c4ddc81d9b095aa0b53a224d33ccb28e9b6c7bffae692dd9","hash":"0db27a53b6c523a179cc2c6743bbd42fa32dcd79ad0acecde15f1a96dfe1e7a4"},{"id":"p1","type":"github","url":"https://github.com/jarnedemeulemeester/findroid/issues/1016","title":"Add support for Cloudflare Access Service Tokens (Custom Headers)","author":"kennypy","publisher":"GitHub — jarnedemeulemeester/findroid","date":"2025-07-20","quote":"While browser access works (via Google SSO), the app fails because it can’t pass the required authentication headers to bypass Cloudflare Access. This limits Findroid to LAN-only use","summary":"A native Jellyfin client cannot use an Access-fronted deployment because it lacks a custom-header extension point.","claim_ids":["c8"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"0db27a53b6c523a179cc2c6743bbd42fa32dcd79ad0acecde15f1a96dfe1e7a4","hash":"4733182f1284165d252fb9053e816b99165ebe4433fefae1db2bf19fb4648a19"},{"id":"p2","type":"github","url":"https://github.com/argoproj-labs/mcp-for-argocd/issues/115","title":"Support custom HTTP headers on outbound ArgoCD API requests","author":"hippiuS","publisher":"GitHub — argoproj-labs/mcp-for-argocd","date":"2026-05-13","quote":"Requests bypass the proxy auth and get a 302 to the SSO page (which a non-browser MCP client can't follow) or a 403.","summary":"An MCP client cannot reach ArgoCD behind Access without a way to supply service-token headers.","claim_ids":["c8"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"4733182f1284165d252fb9053e816b99165ebe4433fefae1db2bf19fb4648a19","hash":"a0ef23ca39c941abb679fb186e7773392ffcd601f4802d953f767ec8fb63c31b"},{"id":"p3","type":"independent_measurement","url":"https://github.com/dataGriff/outcome-app-pattern-whiskey/issues/4","title":"Give Access service-token callers a usable identity","author":"dataGriff","publisher":"GitHub — dataGriff/outcome-app-pattern-whiskey","date":"2026-07-12","quote":"Access service-token JWTs carry an empty `sub` and no email, so a machine caller has no user id to write reviews by and no admin standing — the post-deploy smoke breaks the moment Access is enforced.","summary":"An independent implementation report showing the application-level identity gap after edge authentication succeeds.","claim_ids":["c9"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"a0ef23ca39c941abb679fb186e7773392ffcd601f4802d953f767ec8fb63c31b","hash":"55affd12c300f72e0bbe71483ff01c7e87a3ab2c104d6d0fc1c0436ddb81fa69"},{"id":"p4","type":"github","url":"https://github.com/lesbass/ai-newsroom/issues/4","title":"Cloudflare Access blocker prevents Paperclip API operations","author":"lesbass","publisher":"GitHub — lesbass/ai-newsroom","date":"2026-07-23","quote":"All Paperclip API calls fail with `RESPONSIBLE_USER_UNAVAILABLE` error due to Cloudflare Access authentication issues.","summary":"A health path works while company-scoped requests fail because the authenticated principal is not mapped into application membership.","claim_ids":["c12"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"55affd12c300f72e0bbe71483ff01c7e87a3ab2c104d6d0fc1c0436ddb81fa69","hash":"911dd108ce3d5371cf52e5aaa9d8a6a8bef8655300a7583147a7a952eff0d21d"},{"id":"p5","type":"people","url":"https://news.ycombinator.com/item?id=31332325","title":"Comment on Cloudflare's Access console","author":"systemvoltage","publisher":"Hacker News","date":"2022-05-10","quote":"Well, except the Access/ZeroTrust app. Not sure why that's a different app that takes 10 seconds to redirect a bazillion times.","summary":"A dated operator report criticizing the Zero Trust console's speed and redirect behavior; used only as historical console evidence.","claim_ids":["c18"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"911dd108ce3d5371cf52e5aaa9d8a6a8bef8655300a7583147a7a952eff0d21d","hash":"04ec9ea8ea4d88e81c817cde201954fb5f8b6c6fece8c316a92f76928e8e259e"},{"id":"p6","type":"people","url":"https://news.ycombinator.com/item?id=35494875","title":"Comment on Browser Isolation pricing","author":"8organicbits","publisher":"Hacker News","date":"2023-04-08","quote":"CloudFlare remote browsers is a $10/user/month add-on [2].","summary":"A dated operator price observation, explicitly separated from the current plan page.","claim_ids":["c17"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"04ec9ea8ea4d88e81c817cde201954fb5f8b6c6fece8c316a92f76928e8e259e","hash":"b6cf6643b4b70a32625109d5ad6bd13219af98f6cd568d56bd9ea5b62675d78e"},{"id":"p7","type":"people","url":"https://news.ycombinator.com/item?id=41915668","title":"Comment on Tunnel plus Access","author":"tbhb","publisher":"Hacker News","date":"2024-10-22","quote":"I've been experimenting lately with CloudFlare Tunnel + Zero Trust Access as well for exposing only the endpoints I need from an application for local development like webhooks, with the rest of the site locked behind Access.","summary":"A positive operator report of selectively public webhooks with the rest of an application behind Access.","claim_ids":["c19"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"b6cf6643b4b70a32625109d5ad6bd13219af98f6cd568d56bd9ea5b62675d78e","hash":"ddd1547dd20207f94800abab7ddbe9793088cafa0539c00cdb865dc6a31f1a85"},{"id":"r1","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Temporary Access request-path receipt","publisher":"miscsubjects.com","date":"2026-07-26","quote":"302 before Service Auth; 403 after Service Auth; valid service token reached the origin's 404.","summary":"Redacted first-party request sequence proving edge-policy behavior without publishing the application, policy, token ids, team name or secret.","claim_ids":["c20"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"ddd1547dd20207f94800abab7ddbe9793088cafa0539c00cdb865dc6a31f1a85","hash":"a56e74cdc3557f1f5e27886c1e2c0429a04b1bd067afa7df87dfe6208aab091b"},{"id":"r2","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Service application-token receipt","publisher":"miscsubjects.com","date":"2026-07-26","quote":"RS256; type app; audience and common_name present; sub empty; email absent.","summary":"Bounded fields recovered from the temporary service-token JWT.","claim_ids":["c9"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"a56e74cdc3557f1f5e27886c1e2c0429a04b1bd067afa7df87dfe6208aab091b","hash":"6220755faae91395eb4bd6ae3ae278f57f7880e1fefacd7cfdee91428e3bd237"},{"id":"r3","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Fresh JWKS and owner-gate receipt","publisher":"miscsubjects.com","date":"2026-07-26","quote":"JWKS: HTTP 200, 4,914 bytes, two RSA/RS256 signing keys. /admin without a credential: HTTP 401, 47 bytes, keys error and login.","summary":"Fresh first-party reads of the public team JWKS and the application's bounded unauthorized machine response.","claim_ids":["c11","c15","c21"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"6220755faae91395eb4bd6ae3ae278f57f7880e1fefacd7cfdee91428e3bd237","hash":"1e374e893edeb894b11e1241a7d7e97c4781c9eaec085cf72c928c0b9cb8537a"},{"id":"r4","type":"runtime_receipt","url":"https://miscsubjects.com/api/articles/cloudflare-os-access","title":"Temporary resource deletion proof","publisher":"miscsubjects.com","date":"2026-07-26","quote":"Both authenticated lists succeeded; exact application and service-token ids and names were absent.","summary":"Fresh post-cleanup list proof for the two independent Access collections. No failed response was treated as absence.","claim_ids":["c13","c14"],"accessed_at":"2026-07-26T06:11:43.106Z","prev":"1e374e893edeb894b11e1241a7d7e97c4781c9eaec085cf72c928c0b9cb8537a","hash":"1239356d0617555ed0837ce8d37b72db5a877d7c84b8f9f5f05a089fd787b615"}],"reviews":[],"extra":{"writing_law":"1.3.0","measured_at":"2026-07-26T06:11:43.106Z","content_sha256":"5645f78f08140f785e6c4934b3972bc6e444095e09687728ee7fefb1c01168da","source_chain_head":"1239356d0617555ed0837ce8d37b72db5a877d7c84b8f9f5f05a089fd787b615","disclosure":"Temporary Access resources were deleted; no team name, account id, application id, service-token id, client id, client secret, or owner key is published."},"has_traversal":false,"register":"essay","status":"published","revisions":1,"contributions":[],"provenance":[{"ts":"2026-07-26T06:20:14.182Z","model":"GPT-5.6 (Codex)","action":"write","prompt":"Build the missing Cloudflare OS Access chapter under Writing Law 1.3.0.","input":"Primary Cloudflare documentation, bounded operator evidence, fresh first-party measurements and redacted deletion proof.","response":"22458 characters; 21 claims; 23 sources; 8 widgets.","tokens_in":0,"tokens_out":0,"cost":0,"prev":"genesis","hash":"eaa531169adaca852038ee76e893472ba44bf0018020650c9e1e44d946e2033b"}],"energy":{"passes":1,"tokens_in":0,"tokens_out":0,"tokens_total":0,"cost_usd":0,"models":{"GPT-5.6 (Codex)":1},"head":"eaa531169adaca852038ee76e893472ba44bf0018020650c9e1e44d946e2033b"},"posted_at":"2026-07-26T06:20:14.182Z","created_at":"2026-07-26T06:20:14.182Z","updated_at":"2026-07-26T06:23:26.226Z","machine":{"shape":"article.machine/v1","slug":"cloudflare-os-access","kind":"article","read":{"human":"https://miscsubjects.com/a/cloudflare-os-access","json":"https://miscsubjects.com/api/articles/cloudflare-os-access","bundle":"https://miscsubjects.com/api/articles/cloudflare-os-access/bundle?format=markdown"},"traversal":{"prev":null,"next":null,"hub":null,"series":null,"position":null,"of":null},"ledger":{"claims":21,"sources":23,"contributions":0,"revisions":1,"objections_url":"https://miscsubjects.com/api/articles/cloudflare-os-access/objections","thread_state_url":"https://miscsubjects.com/api/protocol/thread-state?target=cloudflare-os-access","proof_rule":"An action is proven by its ledger receipt, never by a 200 or a description."},"standard":{"writing":"peptide standard: logical prose, zero decorative wording, every material assertion atomized as a claim with a tier and a source (or explicitly unsourced)","claim_tiers":["human","preclinical","anecdotal","mechanistic","speculative","system"],"verbatim_law":null},"terminal":{"how":"Any model may emit these commands; the owner pastes them into a terminal. $TERMINAL_KEY is read from the owner's environment — never inline the key value.","claim_append":"curl -s -X POST https://miscsubjects.com/api/protocol/claim -H \"x-terminal-key: $TERMINAL_KEY\" -H 'content-type: application/json' -d '{\"slug\":\"cloudflare-os-access\",\"text\":\"<one atomized claim>\",\"tier\":\"<human|preclinical|anecdotal|mechanistic|speculative|system>\",\"source_ids\":[],\"who_claims\":\"<model>\",\"rationale\":\"<why material>\"}'","source_append":"curl -s -X POST https://miscsubjects.com/api/protocol/sources -H \"x-terminal-key: $TERMINAL_KEY\" -H 'content-type: application/json' -d '{\"slug\":\"cloudflare-os-access\",\"sources\":[{\"type\":\"review\",\"url\":\"<url>\",\"title\":\"<title>\",\"quote\":\"<verbatim quote>\",\"summary\":\"<one line>\"}]}'","objection":"curl -s -X POST https://miscsubjects.com/api/articles/cloudflare-os-access/objections -H 'content-type: application/json' -d '{\"actor\":\"<model>\",\"objection\":\"<attack>\",\"surface\":\"S1-S8\",\"minimum_patch\":\"<patch>\"}'  # open intake, no key","thread_update":"curl -s -X POST https://miscsubjects.com/api/protocol/thread-update -H 'content-type: application/json' -d '{\"actor\":\"<model>\",\"target\":\"cloudflare-os-access\",\"raw_text\":\"<material delta>\"}'  # open intake, no key","read_back":"curl -s https://miscsubjects.com/api/articles/cloudflare-os-access | python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps(d[\"claims\"][-3:], indent=1))'"}},"representations":{"article":"/a/cloudflare-os-access","json":"/api/articles/cloudflare-os-access","markdown":"/api/articles/cloudflare-os-access/bundle?format=markdown","skill":"/api/articles/cloudflare-os-access/skill","topology":"/api/articles/cloudflare-os-access/topology","versions":"/api/articles/cloudflare-os-access/revisions","invocations":"/api/articles/cloudflare-os-access/invocations"}}}}