To secure Cloudflare Workers, you need to: (1) store secrets using wrangler secret put (never in wrangler.toml), (2) implement rate limiting using Workers KV or Durable Objects, (3) validate all incoming requests including JWT verification at the edge, (4) configure CORS properly to prevent unauthorized origins, and (5) ensure error messages do not leak sensitive information. This blueprint covers edge security patterns for serverless functions.
TL;DR
Workers run at the edge with access to secrets via environment variables. Use wrangler secrets for sensitive data, validate all incoming requests, implement rate limiting with Workers KV or Durable Objects, and be careful with CORS configuration.
JWT Validation at the Edge Cloudflare
export interface Env {
JWT_SECRET: string
ALLOWED_ORIGINS: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// CORS handling
if (request.method === 'OPTIONS') {
return handleCors(request, env)
}
// Authenticate
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 })
}
const token = authHeader.slice(7)
try {
const payload = await verifyJwt(token, env.JWT_SECRET)
// Token is valid, process request
return handleRequest(request, payload, env)
} catch {
return new Response('Invalid token', { status: 401 })
}
},
}
async function verifyJwt(token: string, secret: string) {
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
)
const [headerB64, payloadB64, signatureB64] = token.split('.')
const data = encoder.encode(`${headerB64}.${payloadB64}`)
const signature = base64UrlDecode(signatureB64)
const valid = await crypto.subtle.verify('HMAC', key, signature, data)
if (!valid) throw new Error('Invalid signature')
return JSON.parse(atob(payloadB64))
}
Rate Limiting with KV Cloudflare
export interface Env {
RATE_LIMIT_KV: KVNamespace
}
export async function rateLimit(
ip: string,
env: Env,
limit = 100,
window = 60
): Promise<boolean> {
const key = `rate:${ip}`
const current = await env.RATE_LIMIT_KV.get(key)
if (!current) {
await env.RATE_LIMIT_KV.put(key, '1', { expirationTtl: window })
return true
}
const count = parseInt(current, 10)
if (count >= limit) {
return false
}
await env.RATE_LIMIT_KV.put(key, String(count + 1), { expirationTtl: window })
return true
}
// Usage in worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const ip = request.headers.get('CF-Connecting-IP') || 'unknown'
if (!await rateLimit(ip, env)) {
return new Response('Too many requests', { status: 429 })
}
// Process request...
},
}
Secrets Management
# wrangler.toml - Don't put secrets here!
name = "my-worker"
main = "src/index.ts"
[vars]
ALLOWED_ORIGINS = "https://myapp.com"
# Add secrets via CLI (encrypted, not in source)
# wrangler secret put JWT_SECRET
# wrangler secret put DATABASE_URL
Never put secrets in wrangler.toml. Use wrangler secret put for sensitive values. They're encrypted and only accessible at runtime.
Security Checklist
Pre-Launch Checklist
Secrets stored via wrangler secret
Rate limiting implemented
CORS configured correctly
Request validation in place
Error messages don't leak info
Related Integration Stacks
S3 Secure Uploads Redis Session Management OAuth at the Edge