03 — Secrets & Security Policy¶
Secrets leaking into a public (or even private) repository is the single most common way a cloud account gets compromised. This document defines what counts as a secret, where secrets live, and how we enforce that they never enter source control.
Definitions¶
A secret is any value that, if disclosed, would let an attacker impersonate us, access customer data, or incur billable usage on our accounts. Examples:
- API keys / tokens (Stripe, Cloudflare API tokens, OpenAI, SendGrid, etc.)
- OAuth client secrets
- Database credentials and connection strings that include passwords
- Signing secrets (HMAC keys, JWT secrets, webhook signing secrets)
- Private keys (RSA, Ed25519, SSH, TLS)
- Session-encryption keys
- Turnstile secret keys (the site key is public; the secret key is not)
- Account-specific
CLOUDFLARE_API_TOKENvalues
A non-secret config value is public-safe: public URLs, feature flag booleans, log levels, public API identifiers. These may live in [vars] in wrangler.toml.
When in doubt, treat it as a secret. Moving a value from secrets to vars later is easy; the reverse requires a rotation.
Where secrets live — the three approved stores¶
| Store | Used for | How |
|---|---|---|
| Cloudflare Worker secret store | Everything a Worker needs at runtime. | wrangler secret put NAME --env <env> or the dashboard. |
Local .dev.vars file (gitignored) |
A developer's own dev credentials. Never shared. | File is .dev.vars in the repo root; wrangler dev reads it automatically. Our .gitignore includes it; do not remove that line. |
| CI secrets manager (GitHub Actions / equivalent) | Tokens that CI needs to deploy (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID). |
Repo or org-level secret. |
Anything not in one of those three stores is not allowed to exist. No .env committed. No secrets in Slack, Notion, or email. If a credential was shared via those channels, rotate it after moving it to an approved store.
What must not happen¶
- Secrets in
wrangler.toml[vars]— ever. - Secrets in source files — not even as defaults, examples, or test fixtures. Test fixtures use obviously-fake values (
sk_test_FAKE_FAKE_FAKE). - Secrets in commit messages, PR descriptions, or CI logs.
- Secrets echoed by the Worker itself — never log a secret's value, even masked. Log its presence (
STRIPE_SECRET_KEY present: true) if needed for diagnosis.
Enforcement — automated scans¶
Two scans run on every change. Both must pass before merge and before deploy.
1. Pre-commit hook¶
Installed via the stub's scripts/install-hooks.sh. Runs scripts/preflight-secrets.sh against staged files only. Blocks commit if it finds:
- High-entropy strings longer than 20 characters in non-test files
- Common credential regex patterns (AWS keys, Cloudflare tokens, Stripe keys, GitHub tokens, Slack tokens, private key PEM headers)
.dev.vars,.env, or any file matching*.secretbeing staged
The hook is advisory but mandatory: engineers who bypass it with --no-verify must explain why in the PR. CI re-runs the same scan as a hard gate.
2. CI secret scan¶
ci.yml runs scripts/preflight-secrets.sh against the full diff (git diff origin/main...HEAD). CI fails the build if anything matches. The scan is intentionally simple and pattern-based — it is a safety net, not a replacement for judgement.
For higher-assurance projects (tier-1 per section 05), add one of:
- Gitleaks as a CI step with a tuned config.
- TruffleHog on the full history on a weekly scheduled job.
Both are overkill for most Workers. The inline script is the baseline.
Rotation¶
| Event | Required action |
|---|---|
| Engineer leaves the company | Rotate any secret they may have held. Document the rotation in the platform log. |
| Credential exposed (pushed to a repo, pasted in Slack, screenshot in a doc) | Rotate immediately. Invalidate cached sessions / tokens that depend on it. Post-mortem required. |
| Annual review | Rotate all long-lived tokens that support rotation. Third-party tokens that don't expire get rotated even if nothing happened. |
When rotating a Cloudflare Worker secret, use the staging-first flow:
- Set the new value as the secret in staging:
wrangler secret put NAME --env staging. - Deploy and smoke-test staging.
- Set the new value in production:
wrangler secret put NAME --env production. - Revoke the old credential at the source (Stripe dashboard, etc.).
If a secret leaks — immediate response¶
- Rotate first. Before anything else — dashboard, scrub, apology. The clock is running and the credential is out.
- Invalidate dependent sessions. If the leaked value signs tokens, rotate the signing key and accept that users may need to re-authenticate.
- Purge from history.
git filter-repoor equivalent. Force-push only after coordinating with everyone who has the repo cloned. - Check exposure. Was the repo public at any point? Was the commit pushed to a fork? Review provider-side logs (Cloudflare audit log, Stripe API log, etc.) for any use of the credential between leak and rotation.
- Post-mortem within 5 business days. Blameless, documented, and linked from this SOP's change log.
What the Worker itself must enforce¶
- Input validation on every request. The stub ships with a typed validator pattern in
src/lib/validation.ts— use it. - Deny by default. Unknown routes return 404; unknown methods return 405. Don't let handlers silently respond on paths you forgot about.
- Rate-limit externally-exposed Workers. Cloudflare Rate Limiting rules or WAF rules are preferred over hand-rolled limits. Hand-rolled limits need Durable Objects and careful design.
- Use Turnstile on any Worker that accepts form submissions or un-authenticated mutations. The stub includes a
validateTurnstile()helper wired to our service bindings pattern. - Least-privilege bindings. A Worker that only needs to read from KV gets a read-only API token where the provider supports it; on Cloudflare bindings themselves, scope KV/R2/D1 access to the specific namespace/bucket/db it needs and no other.
Compliance with repo-level Cloudflare features¶
- Turnstile secret keys belong in the Worker secret store. Site keys are public and fine in
[vars]. - Zero Trust service tokens for internal-only Workers go in the secret store too; the client ID may be in
[vars]. - Zaraz — any third-party script keys configured through Zaraz stay in the Zaraz UI, not in Worker code. Document which Zaraz config a Worker assumes.
- Tunnel credentials are per-tunnel and never belong in a Worker repo. If a Worker interacts with a tunneled origin, the Worker only knows the public hostname.
See also: 04 — QA & Deployment for how the scan integrates into the deploy gate.