Skip to main content
Reference Last updated: 31 March 2026

Security

This guide covers cf-monitor's security model, how secrets are managed, and what protections are in place.

This guide covers cf-monitor’s security model, how secrets are managed, and what protections are in place.

Security model

cf-monitor is a cost protection and observability tool, not a security product. It protects your Cloudflare bill by tracking binding operations and tripping circuit breakers when budgets are exceeded. It does not provide WAF, DDoS protection, authentication, or access control for your application workers.

Threat model: cf-monitor defends against accidental cost overruns (infinite loops, misconfigured crons, deployment bugs). The January 2026 incident that inspired cf-monitor was caused by a worker bug writing 4.8 billion D1 rows — not by a malicious actor.

Admin endpoint authentication

The cf-monitor worker exposes /admin/* POST endpoints for operational tasks: manually triggering crons, tripping/resetting circuit breakers, and running dry-run tests. These endpoints are protected by a shared secret (ADMIN_TOKEN).

What ADMIN_TOKEN protects

EndpointWhat it does
POST /admin/cron/{name}Manually trigger any cron handler
POST /admin/cb/tripTrip a circuit breaker on any feature
POST /admin/cb/resetReset a tripped circuit breaker
POST /admin/cb/accountPause/unpause the entire account
POST /admin/test/github-dry-runTest GitHub issue formatting
POST /admin/test/slack-dry-runTest Slack alert formatting

Without ADMIN_TOKEN, these endpoints return 401 Unauthorized. An attacker who discovers your cf-monitor.*.workers.dev URL cannot trip circuit breakers (DoS) or reset them (bypass cost protection).

Setting up ADMIN_TOKEN

Generate a random token and set it as a Worker secret:

# Generate a 32-byte random token
openssl rand -hex 32

# Set it on the cf-monitor worker
npx cf-monitor secret set ADMIN_TOKEN
# Paste the token when prompted

Then include it in requests to admin endpoints:

curl -X POST https://cf-monitor.YOUR_SUBDOMAIN.workers.dev/admin/cron/budget-check \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"

What ADMIN_TOKEN is NOT

  • It is not a Cloudflare API token — it’s a simple shared secret you generate yourself
  • It is not required for the SDK wrapper (monitor()) or consumer workers — only for admin endpoints on the cf-monitor worker
  • It is not used for GET endpoints (/status, /errors, /budgets, etc.) — those are read-only and publicly accessible

Secrets management

cf-monitor uses up to 6 secrets, all set via npx cf-monitor secret set <NAME>:

SecretRequiredPurposeMinimum scope
CLOUDFLARE_API_TOKENYesGraphQL metrics, worker discovery, plan detectionWorkers KV Storage: Edit, Account Analytics: Read, Workers Scripts: Edit. Optional: Account Settings: Read (for plan detection)
ADMIN_TOKENRecommendedAdmin endpoint authenticationN/A — self-generated random string
GITHUB_TOKENOptionalCreate issues for captured errorsFine-grained PAT with issues: write on the target repo. Classic PATs need public_repo (public) or repo (private). Do not use full repo scope if issues: write suffices.
SLACK_WEBHOOK_URLOptionalBudget warnings, error alerts, gap alertsN/A — Slack incoming webhook URL
GITHUB_WEBHOOK_SECRETOptionalVerify GitHub webhook signaturesN/A — self-generated random string, must match the webhook config in GitHub
GATUS_TOKENOptionalBearer token for Gatus heartbeat pingsN/A — provided by your Gatus instance

GitHub PAT minimum scopes

For fine-grained personal access tokens (recommended):

  • Repository access: select only the repo(s) where you want error issues
  • Permissions: Issues: Read and write — nothing else needed

For classic personal access tokens:

  • Public repos: public_repo scope
  • Private repos: repo scope (broader than needed, but the only option with classic PATs)

cf-monitor creates issues, adds labels, and reads issue bodies. It does not need code access, PR permissions, or admin access.

Webhook security

HMAC-SHA256 verification

The POST /webhooks/github endpoint verifies GitHub webhook signatures using HMAC-SHA256 with timing-safe comparison. Requests without a valid X-Hub-Signature-256 header are rejected with 401.

Replay protection

Each webhook delivery includes a unique X-GitHub-Delivery header (a UUID). cf-monitor stores this as a KV nonce with a 24-hour TTL. Replayed webhooks within 24 hours are silently dropped. This prevents an attacker who captures a valid webhook payload from replaying it to manipulate error fingerprint state.

Data exposure

Unauthenticated GET endpoints

These endpoints are publicly accessible (no auth required):

EndpointData exposedData NOT exposed
GET /_healthAccount name, healthy statusAccount ID, worker names
GET /statusAccount name, plan type, healthy status, CB states, worker countAccount ID, worker names, billing period, GitHub repo
GET /errorsError fingerprints, GitHub issue URLsError messages, stack traces
GET /budgetsActive circuit breakers by feature IDBudget limits, usage numbers
GET /workersWorker names and countWorker code, bindings
GET /planPlan type, billing period, allowancesAccount ID
GET /usagePer-service usage numbersAccount ID
GET /self-healthHandler status, error counts, stale cronsInternal state

The /status endpoint intentionally omits the Cloudflare account ID, individual worker names, and GitHub repo path to reduce reconnaissance value.

Consumer worker health endpoint

Each worker wrapped with monitor() exposes /_monitor/health (configurable). This returns the worker name, binding status, and circuit breaker state. It does not return the account ID or binding details.

SDK security

Fail-open design

All SDK code fails open by default. If KV is unreachable, AE writes fail, or any internal error occurs, the consumer worker’s response is not affected. Monitoring should never be the thing that breaks production.

Binding proxy isolation

cf-monitor’s own KV and AE bindings (CF_MONITOR_KV, CF_MONITOR_AE) are excluded from proxy wrapping. The SDK never tracks its own operations, preventing feedback loops.

Path normalisation

Auto-generated feature IDs strip sensitive content from URL paths:

  • Numeric segments (/users/123 becomes users)
  • UUIDs
  • MongoDB-style hex IDs (24+ characters)
  • Query strings (stripped entirely)
  • Paths limited to 2 segments

This prevents sensitive data (user IDs, tokens in paths) from appearing in feature IDs, KV keys, or AE data.

Module-private symbol

The internal tracking metadata (metrics, feature ID, worker name) is stored on the env proxy using a module-private Symbol(). This is not discoverable by other code in the isolate, preventing malicious npm dependencies from reading internal metrics or worker names.

Configuration security

Since v0.3.6, cf-monitor.yaml is embedded as a CF_MONITOR_CONFIG JSON var in wrangler.jsonc by the CLI.

CF_MONITOR_CONFIG is a plaintext wrangler var — it is visible in the Cloudflare dashboard and in wrangler.jsonc. It never contains actual secret values.

Secrets are stored as $VARIABLE_NAME references in the config JSON (e.g. "token":"$GITHUB_TOKEN"). At runtime, parseConfig() resolves these references against the worker’s env object, where secrets live as encrypted wrangler secrets.

The enrichEnv() function has a $-prefix safety check: if a $REFERENCE cannot be resolved (because the corresponding secret is not set), it is never written to env. This prevents literal strings like $GITHUB_TOKEN from being used as actual Bearer tokens in API calls.

Precedence: Direct env vars/secrets always take priority over config resolution. If GITHUB_TOKEN is set as a wrangler secret AND referenced in config, the secret value wins.

Binding detection

cf-monitor uses duck-typing to identify Cloudflare binding types at runtime (checking for method signatures like prepare() + batch() for D1, get() + put() + delete() + list() for KV, etc.). This is fragile — a custom object on env matching these signatures would be incorrectly wrapped as a CF binding and tracked in metrics.

Mitigation: Use the excludeBindings option to skip specific env keys from proxy wrapping:

export default monitor({
  excludeBindings: ['MY_CUSTOM_STORE', 'LEGACY_API_CLIENT'],
  fetch: handler,
});

Keys listed in excludeBindings are returned unwrapped (no metric tracking). cf-monitor’s own bindings (CF_MONITOR_KV, CF_MONITOR_AE) are always excluded automatically.

In practice, the risk is low — env bindings are set at deploy time by Cloudflare, and custom objects rarely match CF binding method signatures. But if you have a custom env object with get(), put(), delete(), and list() methods, excludeBindings is the escape hatch.

Error message handling

Truncation

Error messages from tail events are truncated to 500 characters before storage or transmission. This limits the blast radius if error messages contain sensitive data.

Markdown escaping

Error data interpolated into GitHub issue table cells is escaped to prevent markdown injection. Characters like |, backticks, brackets, and exclamation marks are backslash-escaped, preventing an attacker from injecting tracking images, phishing links, or @mentions via crafted console.error() messages.

Fingerprint normalisation

The error fingerprint algorithm normalises messages by replacing:

  • UUIDs with <UUID>
  • Hex IDs (24+ chars) with <ID>
  • Numbers (4+ digits) with <N>
  • Timestamps with <TS>
  • IP addresses with <IP>

This ensures the same logical error produces the same fingerprint regardless of variable content, and that fingerprints don’t contain PII.

npm package security

CheckStatus
files allowlistOnly src/, dist/cli/, worker/, and cf-monitor.schema.json are published
.npmignoreExcludes tests/, .cf-monitor/, .wrangler/, .dev.vars, .github/
No postinstall scriptsOnly prepublishOnly: build:cli (runs before publish, not on install)
Runtime dependencies2: commander (CLI framework), picocolors (terminal colours). Both well-maintained, no known CVEs
No dynamic code executionZero use of Function() constructors or dynamic code generation anywhere in the codebase
No any typesSDK code uses unknown with explicit narrowing throughout

CLI security

Input validation

The secret set command validates secret names against /^[A-Z_][A-Z0-9_]*$/ to prevent shell metacharacter injection. All CLI commands that invoke wrangler use execFileSync (array arguments, no shell interpolation).

Token handling

The --api-token CLI flag passes the token to wrangler as a command-line argument. On multi-user systems, this may be visible in process listings. Prefer environment variables (CLOUDFLARE_API_TOKEN) or wrangler login for authentication.

Known limitations

KV budget accumulation race condition

Budget counters in KV use a read-modify-write pattern without atomicity (KV does not support atomic increment). Under high concurrency, usage may be slightly under-counted. This is mitigated by the hourly budget enforcement cron, which recalculates from Analytics Engine as the authoritative source.

32-bit fingerprint hash

Error fingerprinting uses FNV-1a 32-bit, which has a ~50% collision probability at ~77K unique errors. In practice, most accounts see far fewer unique errors. If collisions become an issue, a future version may upgrade to SHA-256.

Reporting vulnerabilities

If you discover a security vulnerability in cf-monitor, please report it responsibly:

Please do not open public issues for security vulnerabilities.

Was this helpful?

Related Articles