Skip to content

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_TOKEN values

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 *.secret being 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:

  1. Set the new value as the secret in staging: wrangler secret put NAME --env staging.
  2. Deploy and smoke-test staging.
  3. Set the new value in production: wrangler secret put NAME --env production.
  4. Revoke the old credential at the source (Stripe dashboard, etc.).

If a secret leaks — immediate response

  1. Rotate first. Before anything else — dashboard, scrub, apology. The clock is running and the credential is out.
  2. Invalidate dependent sessions. If the leaked value signs tokens, rotate the signing key and accept that users may need to re-authenticate.
  3. Purge from history. git filter-repo or equivalent. Force-push only after coordinating with everyone who has the repo cloned.
  4. 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.
  5. 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.