Adding security headers at the CDN edge for every response is the most effective way to ensure consistent security posture across your entire web presence.

Let’s see this in action with Cloudflare’s Workers. Imagine a simple Worker that injects Strict-Transport-Security and Content-Security-Policy headers.

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const response = await fetch(request);

  // Clone the response to mutate headers
  const newHeaders = new Headers(response.headers);

  // Strict-Transport-Security: Instructs browsers to only communicate with the site using HTTPS.
  // max-age=31536000 is one year in seconds.
  // includeSubDomains ensures all subdomains also use HTTPS.
  // preload allows the domain to be included in browser preload lists for even faster HTTPS adoption.
  newHeaders.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

  // Content-Security-Policy: Mitigates XSS and data injection attacks by defining allowed content sources.
  // default-src 'self' limits all resource types to the same origin.
  // script-src 'self' 'unsafe-inline' allows inline scripts, but this should be refactored to separate files.
  // style-src 'self' 'unsafe-inline' allows inline styles, but should be refactored.
  // img-src 'self' data: allows images from the same origin and data URIs.
  // font-src 'self' allows fonts from the same origin.
  newHeaders.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';");

  // X-Content-Type-Options: Prevents the browser from MIME-sniffing a response away from the declared content type.
  newHeaders.set('X-Content-Type-Options', 'nosniff');

  // X-Frame-Options: Prevents clickjacking attacks by controlling whether a page can be rendered in a <frame>, <iframe>, <nav>, or <object>.
  // DENY means the page cannot be displayed in a frame, regardless of the site attempting to do so.
  newHeaders.set('X-Frame-Options', 'DENY');

  // Referrer-Policy: Controls how much referrer information is sent with requests.
  // no-referrer-when-downgrade is a safe default, preventing sensitive data from being sent to less secure origins.
  newHeaders.set('Referrer-Policy', 'no-referrer-when-downgrade');

  // Permissions-Policy (formerly Feature-Policy): Allows fine-grained control over browser features.
  // camera 'none'; microphone 'none'; geolocation 'none' prevents access to these specific features.
  newHeaders.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: newHeaders
  });
}

This Worker intercepts every incoming request, fetches the original response, and then crafts a new response with the desired security headers added. The new Headers(response.headers) line creates a mutable copy, allowing us to set new headers or append to existing ones.

The core problem this solves is inconsistent security. Without edge-level headers, you’d need to configure each backend service (your web servers, APIs, static file hosts) individually. This is error-prone, as a single misconfigured service can expose your entire application. By centralizing header management at the CDN, you ensure every response, regardless of its origin, benefits from the same security controls.

The mechanism is simple: the CDN acts as a reverse proxy. It receives the client’s request, forwards it to your origin, gets the response, and then modifies it before sending it back to the client. This modification step is where the security headers are injected.

The most surprising thing most people don’t know is that Strict-Transport-Security can be "preloaded" by major browsers. If you set preload in your HSTS header and submit your domain to browser-specific preload lists (like hstspreload.org), browsers will hardcode visiting your site over HTTPS before they even make the first DNS lookup for your domain, bypassing any potential insecure redirects or initial HTTP requests entirely.

The next step is to understand how to dynamically generate Content-Security-Policy based on the actual resources being loaded by your application.

Want structured learning?

Take the full Cdn course →