KB / Memory / tech-access-missing-app-fails-open
Cloudflare Access fails OPEN when the app is missing — clients keep sending headers nobody checks
A service-token-protected origin becomes publicly reachable if the Access application is absent; clients still send CF-Access-Client-* headers, so nothing errors and it looks healthy
Cloudflare Access enforcement lives in the Access application, not in the client. If the application is missing — deleted, or never recreated after an account move — the hostname is served without any check at all.
Why it hides: well-behaved clients keep sending CF-Access-Client-Id /
CF-Access-Client-Secret. Requests succeed, dashboards look normal, no error is
logged anywhere. The credentials are present; nothing is verifying them. An origin
whose only remaining defence is its own application password is now on the open
internet.
Detection — one unauthenticated request:
curl -sI https://<host>/ # expect 302 -> <team>.cloudflareaccess.com, or 403
A 200 means there is no Access app in front. Service tokens are account-scoped, so
after an account migration the old token id is meaningless on the new account even
though clients still transmit it.
Verify both directions. Proving the gate blocks anonymous traffic says nothing about whether it still admits the legitimate client. Check the pair:
| Request | Expected |
|---|---|
| no credentials | 302 / 403 |
| app credentials, no Access headers | 302 / 403 |
| app credentials + Access headers | 200 |
Ordering when rotating Access credentials: create them → propagate to every client
(Worker vars, secrets, .env) → then create or update the Access application.
Reversed, every client is locked out for the length of the gap. That matters most
where the client fails open by design (backup/telemetry sinks that swallow errors):
the data silently stops arriving instead of raising anything.
A wildcard app covers subdomains, but a more specific app wins. Before relying on
*.example.com to protect a new hostname, confirm no narrower app exists for it.
Related trap: a token that lacks read scope on access/identity_providers or on the
access/apps/{id}/policies sub-resource returns an empty list rather than 403, so
"no identity providers configured" and "cannot see them" are indistinguishable.
Policies are readable inline from the access/apps list response. And Access's
built-in email one-time-PIN needs no identity provider object at all, so an empty
provider list does not mean login is broken.