guide · computers

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.

July 14, 2026 · By Alastair Fraser

A friendly retro robot at a velvet-roped entrance holding a clipboard VIP list, with some emails visible and others crossed out as PUBLIC, plus a golden key labeled SERVICE TOKEN hanging on a hook beside it.

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 everyone as 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_domain is 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

#CommandWhat it proves
1curl -sS -I https://mockups.nowandthenshirts.com/ | head -1403 from anon attempt
2curl -sS -I -H "Cf-Access-Client-Id: $ID" -H "Cf-Access-Client-Secret: $SECRET" https://mockups.nowandthenshirts.com/ | head -1200 with service-token
3Zero Trust > Logs shows the attemptAudit working
4/var/log/access-cron.log next runCron got through
5Token name visible in dashboard > Service AuthToken registered, not pending
6cf-access-id and cf-access-secret rotated onceRotation 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.

Sources

#cloudflare#access#zero-trust#policy#audit#internal

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.

Your email address will not be published. Required fields are marked.