Cloudflare Access Policies for Internal Tools
Put Cloudflare Access (Zero Trust) in front of an internal tool: app definition, Allow policy bound to an email domain, service-token back door for crons, and the audit log to subscribe to.

What Cloudflare Access does
Cloudflare Access sits between the public tunnel and your service. Anonymous requests get a 403 from the edge; requests with a valid JWT for an allow-listed identity get through.
This is the right setup for any internal-only URL — admin panels, mockup tools, staging environments — that you’d otherwise expose openly. It also works cleanly in front of a Cloudflare Tunnel pointing at a service bound to localhost: the operator gets authentication without writing any auth code in the service itself.
The price: every request now goes through Cloudflare’s edge for an identity check (typically adds 10-30ms on first hit, sub-1ms once JWT is cached). The free tier covers up to 50 users per account; the per-seat-plan starts when you cross that.
Step 1: define the app
Prerequisite: the hostname is fronted by a working Cloudflare Tunnel (or a Cloudflare-managed DNS record) — Access decisions happen at the edge, so without a Cloudflare proxy in front, the policy never fires.
In the Cloudflare Zero Trust dashboard, navigate to Access > Applications > Add an application > Self-hosted. Set:
- Application name: anything you like (e.g.
nowandthenteam-mockups) - Domain: the hostname you want protected (e.g.
mockups.nowandthenshirts.com) - Identity providers: One-time PIN — free, ships with every Zero Trust account, requires only an email address. If you also want Google/Microsoft SSO, enable them in Settings > Authentication before adding the app; they’re not on by default.
Save the application. The dashboard then forwards you to a Policy tab where the first policy lives. Don’t skip past that; the app exists at zero policy and would 403 every visitor.
Step 2: the Allow policy
The first policy: anyone whose email is in the now-and-then shirts org domain, with the One-time PIN identity provider. Anyone outside that domain — including the public — fails with 403.
The policy JSON shape (in the dashboard or via API):
{
"name": "nowandthenteam",
"decision": "allow",
"include": [
{"email_domain": "nowandthenshirts.com"}
]
}
For now, this lets anyone with a @nowandthenshirts.com email through. Multiple team orgs would each get their own policy with a higher priority and a narrower email_domain.
Precedence notes: policies evaluate top-down. An Allow rule followed by a Block rule on the same user lets them through — Block is the inverse of precedence, not a separate rule. The “Block everyone not on this email domain” pattern is the wrong mental model; the right pattern is “Allow this email_domain, and if you don’t match any policy, get 403 anyway.”
Step 3: the service-token back door
Cron jobs and automations also need to reach the service. Create a service token in the same dashboard under Access > Service Auth > Create a token, then add the token to the policy’s include:
"include": [
{"email_domain": "nowandthenshirts.com"},
{"service_token": ["<token-id>"]}
]
The cron’s job then sends two headers with every request:
Cf-Access-Client-Id: <token-id>
Cf-Access-Client-Secret: <token-secret>
The edge issues a short-lived JWT (4-hour TTL by default) the first time, then validates the JWT on subsequent requests without re-checking the dashboard. This is how the Operator’s house cron (a 5:30 AM Telegram briefing job, for example) reaches mockups.nowandthenshirts.com without sending a PIN email to itself every morning.
Service token rotation: the secret is one-line rotatable from the dashboard; plan to rotate every 6 months. Update the cron’s secret file (/opt/data/credentials/<token-secret>.txt or equivalent) at the same moment, otherwise the next cron tick gets 403.
Step 4: the audit log
Access ships every access attempt to the dashboard at Logs > Access. Set up a daily email digest or feed the JSON to a SIEM:
# Pull the last 24h of access logs
curl -sS -H "Authorization: Bearer ${CF_TOKEN}" \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/audit_logs?since=$(date -d '24 hours ago' +%s)" | jq
The log row includes the user identity, the resource requested, the IP, and the decision. Free-tier retention is 30 days; longer retention means enabling the add-on.
Two patterns the operator should subscribe to:
- Repeated denied attempts from one IP within an hour. Either a misconfigured cron or a probing bot. Pairs well with Cloudflare’s WAF block action.
- Service token used from a new IP. A leaked secret; rotate immediately.
What not to do
- Don’t put
Include everyoneas the policy — that’s the same as no policy. - Don’t bind the policy to a single email when you mean to mean a whole team —
email_domainis the right primitive. - Don’t enable Access on a public-facing page — Cloudflare’s Free tier is 50 users; pure public traffic will burn through that limit and the cost becomes Painful.
- Don’t forget the service token — if you ship a cron job before the token is wired, the cron will get 403s every run.
- Don’t edit the application’s domain (the protected hostname) without first removing the access policy — saving the change with the wrong domain silently lets through anyone on the original domain.
- Don’t skip the audit-log subscription — without it, you won’t know about leaked service tokens until a probe finds them.
Verification checklist
| # | Command | What it proves |
|---|---|---|
| 1 | curl -sS -I https://mockups.nowandthenshirts.com/ | head -1 | 403 from anon attempt |
| 2 | curl -sS -I -H "Cf-Access-Client-Id: $ID" -H "Cf-Access-Client-Secret: $SECRET" https://mockups.nowandthenshirts.com/ | head -1 | 200 with service-token |
| 3 | Zero Trust > Logs shows the attempt | Audit working |
| 4 | /var/log/access-cron.log next run | Cron got through |
| 5 | Token name visible in dashboard > Service Auth | Token registered, not pending |
| 6 | cf-access-id and cf-access-secret rotated once | Rotation process rehearsed |
Sources
Last verified: 2026-07-14 against the live mockups.nowandthenshirts.com Access policy. Service-token rotation cadence recorded at the operator’s rotation cron schedule.

Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.