Cloudflare Access — Service Token Setup for Bot Access
Context
SecurityV0 uses Cloudflare Zero Trust (Access) to protect web properties. Human users authenticate via email OTP or GitHub OAuth. Bots and CI pipelines need programmatic access via Service Auth tokens — a CF-Access-Client-Id + CF-Access-Client-Secret header pair that bypasses the login page.
This runbook covers adding Service Auth policies to Access applications so bots can reach protected sites.
Prerequisites
- Admin access to the Cloudflare Zero Trust dashboard (
https://one.dash.cloudflare.com) - The SecurityV0 account selected
Current State
| Site | Access Application | Service Auth | Status |
|---|---|---|---|
app.securityv0.com | SecurityV0 Production | ✅ ci-deploy-bot | Working |
dev.securityv0.com | SecurityV0 Dev | ✅ ci-deploy-bot, visual-review-bot | Working |
sv0-reviews.pages.dev | sv0-reviews | ✅ GitHub org policy | Working |
sv0-docs.pages.dev | sv0-docs | ❌ No service token policy | Needs fix |
Step-by-Step: Add Service Auth to an Access Application
1. Create a Service Token (if one doesn't already exist)
- Go to Zero Trust → Access → Service Auth
- Click Create Service Token
- Name it descriptively (e.g.,
bot-docs-access) - Set duration (recommend 1 year, with a calendar reminder to rotate)
- Copy both values immediately — the secret is shown only once:
CF-Access-Client-Id→ store asCF_ACCESS_CLIENT_IDCF-Access-Client-Secret→ store asCF_ACCESS_CLIENT_SECRET
Reuse existing tokens when possible. If
ci-deploy-botalready exists and the same bots need access, just add it to the new application's policy — no need to create a new token.
2. Add a Service Auth Policy to the Access Application
- Go to Zero Trust → Access → Applications
- Find the application (e.g.,
sv0-docs) - Click Edit → go to the Policies tab
- Click Add a policy
- Configure:
- Policy name:
Service Auth - bot access - Action:
Service Auth - Include: Service Token → select the token (e.g.,
ci-deploy-bot)
- Policy name:
- Save the policy
3. Test
# Should return page content (200), not a 302 redirect
curl -sI https://sv0-docs.pages.dev \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
# Verify: without headers, should still 302 to login
curl -sI https://sv0-docs.pages.dev
Expected results:
- With headers:
HTTP/2 200+ page content - Without headers:
HTTP/2 302→ redirect tosecurityv0.cloudflareaccess.com
4. Distribute Credentials
For local development (.env) — canonical runtime source:
sv0-platform/.envis the canonical runtime source forCF_ACCESS_CLIENT_IDandCF_ACCESS_CLIENT_SECRET. Every agent / script / scan reads from here at runtime.- Scripts like
visual-screenshot.tsread these automatically viaprocess.env(loaded byscripts/lib/load-env.ts). - Run
source .envbefore using curl, or usedotenvin scripts. - 1Password is rotation backup only — never the runtime source. If
.envis missing the variables, Ivan re-populates them from 1Password (see step below). Agents must not read from 1Password directly (seefeedback_no_1password_accessmemory).
For CI/CD (GitHub Actions):
- Add to repo secrets:
CF_ACCESS_CLIENT_ID,CF_ACCESS_CLIENT_SECRET - Reference in workflows via
${{ secrets.CF_ACCESS_CLIENT_ID }}
For 1Password (backup/rotation):
- Tokens are stored in the SecurityV0 vault under "Cloudflare Access — visual-review-bot" and "Cloudflare Access — ci-deploy-bot"
- Use
op item get "Cloudflare Access — visual-review-bot" --format jsonto retrieve
For bot agents (OpenClaw):
- Add to the bot's environment variables
- Bots use these headers when fetching protected URLs
Usage in Code
curl
curl https://sv0-docs.pages.dev \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
Node.js fetch
const response = await fetch("https://sv0-docs.pages.dev", {
headers: {
"CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID!,
"CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET!,
},
});
Playwright (for visual review / screenshots)
const context = await browser.newContext({
extraHTTPHeaders: {
"CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID!,
"CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET!,
},
});
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Still getting 302 with headers | Service Auth policy not added to the application | Check Access → Applications → Policies |
403 Forbidden | Token exists but not included in the app's policy | Add the token to the application's Service Auth policy |
401 Unauthorized | Token expired or secret wrong | Regenerate token in Access → Service Auth |
Works for dev.securityv0.com but not sv0-docs.pages.dev | Different Access applications — each needs its own policy | Add the token to the sv0-docs application specifically |
Token Rotation
Service tokens should be rotated periodically:
- Create a new token in Access → Service Auth
- Add the new token to all relevant Access application policies
- Update secrets in GitHub Actions and bot environments
- Verify access works with the new token
- Remove the old token from policies, then delete it
Set a calendar reminder for rotation (recommend annually for non-production, quarterly for production).
-- Echo (sv0-echo)