TL;DR
Cloudflare Pages runs at the edge with DDoS protection and automatic SSL built in. Put secrets in environment variables (they're encrypted), check authentication inside your Pages Functions, and add WAF rules on top if your plan has them. Preview deployments are public. Protect anything sensitive.
Cloudflare's Security Advantage
Deploy on Pages and you inherit a lot of Cloudflare's security infrastructure without configuring anything:
- Global edge network: Content served from 300+ locations worldwide
- DDoS protection: Automatic mitigation at no extra cost
- Automatic SSL: Free TLS certificates for all domains
- WAF integration: Web Application Firewall (on paid plans)
- Bot management: Protection against automated attacks
Environment Variables
Production vs Preview Variables
You can set different variables for production and preview. Do it:
# Production only
API_KEY=prod_key_xxxxx
# Preview only
API_KEY=test_key_xxxxx
# Both environments
NEXT_PUBLIC_SITE_URL=https://example.com
Use different secrets for preview and production. Preview deployments are public. If a preview secret leaks, you want it to be one that can't touch real data.
Accessing Variables in Functions
// functions/api/data.js
export async function onRequest(context) {
// Access environment variables from context.env
const apiKey = context.env.API_KEY;
const dbUrl = context.env.DATABASE_URL;
// Use the secrets...
const response = await fetch('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
return new Response(JSON.stringify(await response.json()), {
headers: { 'Content-Type': 'application/json' }
});
}
Pages Functions Security
Authentication in Functions
// functions/api/protected.js
export async function onRequest(context) {
const authHeader = context.request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const token = authHeader.substring(7);
try {
// Verify the token (example with JWT)
const payload = await verifyJWT(token, context.env.JWT_SECRET);
// Continue with the request
return new Response(JSON.stringify({ user: payload.sub }), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid token' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
}
Input Validation
// functions/api/submit.js
export async function onRequest(context) {
if (context.request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
let body;
try {
body = await context.request.json();
} catch {
return new Response(JSON.stringify({ error: 'Invalid JSON' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Validate required fields
if (!body.email || typeof body.email !== 'string') {
return new Response(JSON.stringify({ error: 'Email required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(body.email)) {
return new Response(JSON.stringify({ error: 'Invalid email' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Process valid input...
}
Security Headers
The simplest way to add security headers is a _headers file at the root of your build output. Pages Functions work too:
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
/api/*
Access-Control-Allow-Origin: https://yourdomain.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Cloudflare Access Integration
Cloudflare Access puts a login in front of a whole site or path. With it you can:
- Protect preview deployments with login
- Restrict access to specific email domains
- Add MFA requirements
- Integrate with identity providers (Google, Okta, etc.)
Cloudflare Access sits in front of your application, so nobody reaches the site until they've signed in. It's the right fit for internal tools and staging environments.
WAF Configuration
On Cloudflare Pro and above you can write Web Application Firewall rules. Common ones:
- Block SQL injection attempts
- Prevent XSS attacks
- Rate limit aggressive requests
- Block known bad user agents
# Block requests with SQL injection patterns
Rule: (http.request.uri.query contains "UNION SELECT")
Action: Block
# Rate limit API endpoints
Rule: (http.request.uri.path matches "^/api/")
Rate Limit: 100 requests per minute per IP
Cloudflare Pages Security Checklist
Before Going to Production
All secrets in environment variables
Different secrets for preview vs production
Functions validate all input
Functions check authentication where needed
Security headers configured
CORS limited to your domain
Consider Cloudflare Access for sensitive paths
WAF rules reviewed (if available)
Are my environment variables secure on Cloudflare Pages?
Yes. Cloudflare encrypts them at rest, and only your Pages Functions can read them at runtime. They don't end up in your static build output.
Can I protect preview deployments?
Preview deployments are public by default. Use Cloudflare Access to require authentication for preview URLs, or use different (non-sensitive) environment variables for previews.
How do Pages Functions compare to Workers?
Pages Functions are Workers under the hood, so they run at the edge with the same security model. The difference is setup: Functions give you file-based routing inside a Pages project with no extra config.
Does Cloudflare see my traffic?
It processes requests on your behalf, so yes, traffic passes through Cloudflare. If you need end-to-end encryption for sensitive data, encrypt it client-side before it's sent. Cloudflare holds security certifications and doesn't access your data inappropriately.
Deploying to Cloudflare Pages?
Scan your project for security issues before going live.