Cloudflare Setup: A Read-Only API Token
This guide sets up a read-only Cloudflare API token for the LeftSize scan. The token is stored as a GitHub secret in your repository and is only used on your GitHub runner.
Why an API token and not OIDC
On AWS and Azure, LeftSize signs in with OpenID Connect (OIDC), so no secret is stored. Cloudflare has no OIDC or workload identity federation for API tokens, so a read-only API token stored as a GitHub secret is the only way to sign in.
The token never reaches LeftSize. The action reads it on your runner, calls the Cloudflare API with it and sends LeftSize only the findings, the same as with AWS and Azure credentials. See Security and Data.
Prerequisites
- A Cloudflare account with zones or other resources to scan
- Permission to create account API tokens: Super Administrator, or API Token Provisioning on the account
- A GitHub repository with LeftSize installed
Step 1: Create an account-owned, read-only token
Use an account-owned token (an account API token). It isn’t tied to a person, so the scan keeps working when the person who created it leaves the team.
- Open the LeftSize token template). It opens Create account API token in the Cloudflare dashboard with the name
LeftSize (read-only)and the permissions below filled in. The onboarding page at leftsize.com gives you the same link. - If you have access to more than one account, pick the account to scan.
- Under the zone resources, include all zones of the account, so new zones are scanned too.
- Optionally set a TTL (end date); see Rotating the token.
- Click Continue to summary, check the permissions, then Create Token.
- Copy the token. Cloudflare shows it only once.
To create the token by hand instead, go to Manage Account > Account API tokens > Create Token, choose a custom token and add these permissions, all set to Read:
Token permissions
| Scope | Permission | Used for |
|---|---|---|
| Zone | Zone | Listing zones, their plan and status. The action uses it to check the token before it scans; without it, the zone rules don’t run (see Verifying the setup). |
| Zone | Zone Settings | SSL/TLS mode, minimum TLS version, Always Use HTTPS, HSTS, Auto Minify |
| Zone | DNS | DNSSEC status |
| Zone | SSL and Certificates | Origin CA and custom certificates |
| Account | Account Settings | Two-factor enforcement and members without two-factor authentication |
| Account | Account Analytics | Worker request counts, Workers AI usage per model |
| Account | Account API Tokens | Account API tokens: last use, permissions, expiry, IP restrictions |
| Account | Workers Scripts | Workers and their settings |
| Account | Workers R2 Storage | R2 buckets: r2.dev access, CORS, lifecycle rules |
| Account | Access: Apps and Policies | Access applications and their policies |
| Account | Access: Service Tokens | Access service tokens |
| Account | Access: Audit Logs | Zero Trust users and their seats (the permission Cloudflare’s users API requires) |
| Account | Access: Organizations, Identity Providers, and Groups | The seat expiration setting |
| Account | Cloudflare Tunnel | Tunnels and their connection status |
| Account | Load Balancing: Monitors and Pools | Load balancer pools |
All of these are read permissions. With them LeftSize can’t change anything in your account.
Page Rules and Registrar: a user API token
Cloudflare doesn’t support Page Rules and Registrar with account-owned tokens. Three rules need them:
leftsize-cloudflare-page-rules-deprecatedneeds Page Rules Read (zone)leftsize-cloudflare-domain-expiringandleftsize-cloudflare-domain-transfer-unlockedneed Registrar Domains Read (account)
With an account-owned token these three rules don’t run; the rest of the scan does. To include them, create a user API token instead (My Profile > API Tokens > Create Token) with the permissions in the table above plus Page Rules Read and Registrar Domains Read. A user API token can do everything the scan needs. The trade-off: it belongs to a person and acts with that person’s access, so it stops working when that person loses access to the account.
Step 2: Add the secret to GitHub
In your repository, go to Settings > Secrets and variables > Actions.
Secrets
| Secret | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
The API token from Step 1 |
LEFTSIZE_INSTALLATION_ID |
Provided during LeftSize onboarding |
LEFTSIZE_REPOSITORY_TOKEN |
Provided during LeftSize onboarding |
Variables (optional)
| Variable | Value |
|---|---|
CLOUDFLARE_ACCOUNT_IDS |
Account IDs to scan, comma-separated. Leave it empty to scan every account the token can read. |
Step 3: Add the workflow
The onboarding page generates this workflow. Commit it as .github/workflows/leftsize.yml:
name: LeftSize Cloudflare Scan
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
leftsize-cloudflare:
runs-on: ubuntu-latest
steps:
- name: Run LeftSize Scan
id: leftsize
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: cloudflare
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-ids: ${{ vars.CLOUDFLARE_ACCOUNT_IDS }}
verbose: false
- name: View Results Summary
if: always()
run: |
echo "LeftSize Cloudflare scan complete!"
echo "Found ${{ steps.leftsize.outputs.findings-count }} findings"
echo "Results submitted: ${{ steps.leftsize.outputs.findings-submitted }}"
There is no login step: the action signs in with the token itself. The job needs no id-token: write permission, because it doesn’t use OIDC. To scan AWS or Azure from the same workflow, add a job per cloud; see Scanning several clouds from one workflow.
Scanning some accounts only
Without cloudflare-account-ids, the scan covers every account the token can read. To limit it, set the CLOUDFLARE_ACCOUNT_IDS variable to one or more account IDs, comma-separated. An account ID is 32 hexadecimal characters. In the dashboard, copy it from the API section of a domain’s Overview page, or from Account Details under Workers & Pages. An invalid ID stops the step with Invalid Cloudflare account ID(s): expected 32 hex characters.
Findings are grouped per Cloudflare account: each rule gets one issue per account, so issues from different accounts never mix.
Rotating the token
The token is a long-lived credential stored in GitHub. Limit what a leaked token can do:
- Set a TTL. Give the token an end date when you create it, and create a new one before it expires. When it has expired, the scan fails with an authentication error until you update the
CLOUDFLARE_API_TOKENsecret. - Roll the token when someone who had access to it leaves, or when you think it leaked. Rolling gives the token a new value with the same permissions and invalidates the old value, so update the secret right after.
- Client IP filtering is not practical with GitHub-hosted runners, because their IP addresses change. Use it only with self-hosted runners that have fixed addresses.
LeftSize itself flags account API tokens that can make changes and have no expiry and no IP restriction (leftsize-cloudflare-api-token-unrestricted). The LeftSize token has read permissions only, so it isn’t flagged.
Verifying the setup
- Go to your repository’s Actions tab
- Select LeftSize Cloudflare Scan
- Click Run workflow
- Check that the job summary lists the scan results
Before it scans, the action checks that the token can list zones. If it can’t, the action asks Cloudflare whether the token is active: a user token is checked directly, an account-owned token only for the accounts in cloudflare-account-ids. An active token without Zone Read still scans the account-level rules (Workers, R2, API tokens, Zero Trust and so on); the zone rules are listed as not run. If the token isn’t confirmed active either, the step stops with Cloudflare authentication failed: the API token can't list zones (it needs Zone Read) and isn't confirmed active. For an account-owned token, also set cloudflare-account-ids. Check that:
- The
CLOUDFLARE_API_TOKENsecret holds the token value, not its name or ID - The token is active and hasn’t expired
- The token has Zone Read and includes the zones of the account, or, for an account-level scan only, that
cloudflare-account-idsis set when you use an account-owned token
Missing permissions
A missing permission stops only the rules that need it. The rest of the scan runs. Those rules are listed under “Incomplete scan” in the job summary and in the failed-policies output; set LEFTSIZE_DEBUG: true on the scan step to see the error of each (see Troubleshooting).
Some Cloudflare list APIs don’t return an error without permission, but an empty list. This applies to Workers, Access applications, Access service tokens and tunnels. The rule then finds nothing, and the step log shows a note such as:
No Workers listed for account <id>. If it has Workers, the API token is missing the Workers Scripts Read permission.No <kind> found in account <id> (if there should be, check the API token's permissions)
If the account does have such resources, add the permission from the table above to the token.
What isn’t scanned
- Logpush jobs. Cloudflare requires the Logs Write permission for every Logpush API call, including listing jobs. LeftSize stays read-only, so it doesn’t check Logpush.
- Firewall Rules and the previous rate limiting rules. Cloudflare already converted them to WAF custom rules and rate limiting rules, and their API endpoints no longer work.
leftsize-cloudflare-zone-legacy-featureschecks Auto Minify instead. - Page Rules and Registrar domains with an account-owned token; see Page Rules and Registrar.
What LeftSize reads
- Configuration of zones, Workers, R2 buckets, API tokens, Zero Trust, tunnels, load balancer pools and certificates through the Cloudflare API
- Worker request counts and Workers AI usage per model (request counts only, never prompts or responses) through Cloudflare’s GraphQL Analytics API
- Never creates, changes or deletes anything
- Never reads the data inside your resources (no R2 objects, no KV or D1 contents), Worker secrets, or private keys
- Some API responses include more than a rule needs: Worker settings include the values of plain-text variables, and certificate details include the certificate (the public part, not the key). The action reads these on the runner, keeps only what the rules need, and never puts the values or certificates in findings, so they aren’t sent to LeftSize. For plain-text variables that look like secrets, only the variable names are sent
Was this article helpful?