Secure by Default on Cloudflare Pages: Headers, WAF & Analytics Without a Server

8/9/2026

This blog is a static site: Next.js exported to plain HTML, served from Cloudflare Pages. There's no database, no server process, no login. A common assumption follows: nothing to secure.

That's wrong in a useful way. A static site has a smaller attack surface, not a zero one. It can still ship missing security headers, leak referrer data, get framed into clickjacking, or serve mixed content. The good news is that on a serverless edge host, a solid baseline is mostly configuration, not code. And it costs nothing. Here's what I run.


1. Response headers: the cheapest security you'll ever ship

Security headers tell the browser how to behave defensively. On Cloudflare Pages you set them with a _headers file in your output directory. The baseline I use:

/*
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

That block isn't aspirational: it's copied from this site's live _headers file, warts and all. More on the warts below.

What each one buys you:

  • X-Content-Type-Options: nosniff: stop the browser from guessing (and mis-executing) content types.
  • X-Frame-Options: DENY + frame-ancestors 'none': no one can iframe your site into a clickjacking trap.
  • Referrer-Policy: don't leak full URLs to third parties via the Referer header.
  • Permissions-Policy: explicitly switch off device APIs a blog never needs.
  • HSTS: force HTTPS for two years, including subdomains. (Only add preload once you're sure every subdomain is HTTPS. It's hard to undo.)
  • Content-Security-Policy: the big one. Constrain where scripts, styles, and images may load from. This is your strongest defence against injected/XSS content.

A note on CSP and static-site generators (and my own documented exceptions). Frameworks often need 'unsafe-inline': Next.js static export emits inline bootstrap and hydration scripts, so this site keeps 'unsafe-inline' in both script-src and style-src. Hash-based allowlisting exists but is brittle across builds, so I accept the trade-off and write it down, which is the actual rule: tighten as far as your build allows, test in report-only mode first (Content-Security-Policy-Report-Only), and treat every unsafe-* you keep as a documented exception rather than a default. The cloudflareinsights.com entries are the second exception: they allow exactly one thing, the cookieless analytics beacon described below.

Verify, don't assume. After deploy, check the live headers with curl -I https://your-site or any online header scanner. A header you think you set but didn't is the most common failure here.


2. The edge WAF: protection your origin doesn't have to run

Even with no backend, traffic still hits your domain: bots, scrapers, vulnerability scanners probing for /wp-admin and friends. Cloudflare's managed WAF rules and bot controls sit at the edge, in front of your static assets, and cost nothing on the free tier for the basics:

  • Turn on the managed ruleset for common attack patterns.
  • Enable bot fight mode to shed the obvious automated noise.
  • Add a couple of custom rules if you see specific abuse (rate-limit a path, block a region you don't serve).

The point isn't that a static site is vulnerable to SQL injection (it isn't). The point is hygiene: you drop garbage traffic before it ever touches your deploy, and you get visibility into who's knocking.


3. Analytics without surveilling your readers

You can know your traffic without inheriting a privacy liability. Cloudflare Web Analytics is cookieless and doesn't fingerprint visitors, which means in most regimes (GDPR, PDPA, and friends) you avoid the consent-banner burden entirely, because you're not collecting personal data in the first place.

This is privacy-by-design as a practical choice, not a compliance chore: the most defensible data is the data you never collect. If a privacy-friendly, server-side analytics option does the job, it's both kinder to readers and less for you to govern.


4. The supply chain you forgot you have

The last piece is the one static-site owners overlook: your build pulls dependencies, and your deploy runs through a pipeline. That's your real attack surface now.

  • Pin and update dependencies; enable automated dependency alerts.
  • Lock down who can push to the branch that deploys to production.
  • Scope your deploy tokens narrowly and rotate them: they're machine identities like any other.
  • Keep secrets out of the repo (secret scanning + push protection).

A static site can't be hacked through a backend it doesn't have, but it can absolutely be compromised through a poisoned dependency or a leaked deploy token.


What I'd do next

  • Add the _headers baseline above and verify the live response with curl -I.
  • Run the CSP in report-only for a week, then enforce.
  • Enable the managed WAF ruleset and bot fight mode.
  • Switch on cookieless analytics and delete any heavier tracker.
  • Turn on dependency alerts + secret scanning and lock the production branch.

Closing thought

"Static" means fewer moving parts, not no security posture. The win is that on a serverless edge host, a strong baseline is almost entirely declarative (a headers file, a few toggles, a privacy-respecting analytics choice), and it's free.

The question I'd put to any static site, including this one: "If a browser loaded a malicious response on my domain right now, which header would stop it, and have I actually shipped that header?"