Security Headers via CDN: CSP, HSTS and Permissions-Policy at the Edge

Implementing security headers at the origin is straightforward until you run a site behind a CDN. Suddenly you have two places where headers can be set, cached copies that may or may not include them, and multiple edge PoPs that need consistent behavior. In my experience, the edge is almost always the better place to enforce headers like Content Security Policy (CSP), HTTP Strict Transport Security (HSTS), and Permissions-Policy — it gives you a single control plane, instant global deployment, and independence from application releases. This article covers how I approach security headers, csp, hsts and related controls directly via cdn configuration, what to watch out for as a beginner, and how to avoid the common mistakes that break sites.

Why Injecting Security Headers at the Edge Beats Origin Configuration

When headers are set only at the origin, every response has to travel through the full origin stack before the browser sees the protection. If your origin fails to add a header for a specific route, error page, or static asset, that response goes out unprotected. I’ve seen this happen with 404s and 500s generated by the application server that bypassed the middleware where headers were added. By moving header injection to the CDN, you apply the policy after the cache lookup, on every response that leaves the edge — including cache hits, synthetic error pages, and origin failures served stale.

There are three practical advantages to edge injection. First, consistency. A response headers policy attached at the distribution or zone level applies to all cache behaviors unless you explicitly exclude a path. Second, speed of change. Updating a CDN configuration or Edge Worker is a control-plane operation that propagates globally in seconds or minutes, compared to redeploying application code. Third, separation of concerns. Application teams can iterate on features without worrying about header syntax, while the platform or security team owns the edge policy. That ownership model is how most mature teams I work with operate.

How Edge Injection Interacts With Cache

Headers added at the edge do not automatically become part of the cache key, which is exactly what you want for security headers — you want the same header on both HIT and MISS. However, you do need to decide whether the header should be cached with the object or appended at delivery time. Most modern CDNs do both: they store the origin response and then apply an outbound header modification before sending to the client. Cloudflare Transform Rules, Fastly VCL vcl_deliver, Akamai EdgeWorkers, and AWS CloudFront ResponseHeadersPolicy all work this way. In my experience, appending at delivery time is safer because a purge or origin change cannot strip the header.

One caveat for beginners: if you set headers at both origin and edge, understand the merge behavior. Some CDNs overwrite, some append duplicate headers. Duplicate Strict-Transport-Security or Content-Security-Policy headers will cause browsers to enforce both, which can lead to unexpected blocks. I standardize on one source of truth — the edge — and strip any conflicting headers coming from origin at the CDN.

Crafting a Content Security Policy That Survives Real CDN Traffic

Content Security Policy is the most powerful and the most likely to break your site if you get it wrong. CSP tells the browser exactly where it is allowed to load scripts, styles, images, fonts, and other resources from. A well-tuned policy mitigates cross-site scripting (XSS) and data-injection attacks, but it requires an inventory of what your site actually loads.

For a beginner, I recommend starting with a permissive but meaningful baseline and iterating toward strictness using reporting. In my experience, teams that jump straight to default-src 'none' end up rolling back within an hour because of third-party tags, inline analytics, or legacy jQuery plugins. A better sequence is: collect, report, enforce.

A Sensible Starting Policy for CDN Delivery

Here is a baseline I use for many brochure and content sites behind a CDN. It allows self-hosted assets, images from anywhere over HTTPS, and blocks plugins and framing:

Content-Security-Policy: default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' https: data:;
  connect-src 'self' https://www.google-analytics.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests

This policy is intentionally served from the edge for all text/html responses only. You should not apply CSP to images, CSS, or JS themselves — it does nothing there and wastes bytes. Most CDNs let you scope the header by content type or path pattern. With CloudFront you would attach a ResponseHeadersPolicy to the cache behavior for *.html and /, or use CloudFront Functions to conditionally add it when Content-Type contains text/html.

Nonces, Hashes and Third-Party Scripts

The hardest part of CSP today is handling inline scripts. Modern best practice is to avoid 'unsafe-inline' and use a per-request nonce or hash. When you do this at the edge, you need compute at the edge — a Worker or Edge Function that generates a cryptographically random nonce, injects it into the header and rewrites the HTML to add the matching nonce="..." attribute.

I’ve found that for beginners, it is more practical to start without nonces, keep 'unsafe-inline' for style-src only if needed, and use hashes for a small number of known inline blocks. Once you have good visibility, move to nonces. Always deploy CSP in report-only mode first:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self';
  report-uri https://csp-report.example.com/collect;
  report-to csp-endpoint

Send reports to an endpoint and review for 48-72 hours before enforcing. The reporting API is documented in the HTTP Working Group Specifications at https://httpwg.org/specs/ and explained with examples on https://web.dev/articles/. Pairing CSP with WAF Rules at the CDN Edge: OWASP Core Rule Set and Custom Policies gives you defense in depth — the WAF blocks known attack patterns before they reach the origin, while CSP contains what the browser is allowed to execute if something slips through.

Deploying HSTS From the Edge Without Locking Yourself Out

HTTP Strict Transport Security is a simple header with outsized consequences. When a browser sees Strict-Transport-Security over a valid HTTPS connection, it will refuse to use plain HTTP for that host for the duration of max-age. Preload adds an even stronger commitment by baking your domain into browsers.

The CDN is the ideal place to set HSTS because the CDN terminates TLS. Your SSL/TLS Certificate Management for CDNs: Automation, Renewal and CT Logs and HSTS policy live together. If you set HSTS only at the origin but serve HTTP at the edge, clients will never see it on cache hits.

Choosing max-age and includeSubDomains Safely

For a new site, I use this staged rollout:

Stage 1 — validation (1 day): Strict-Transport-Security: max-age=86400
Stage 2 — ramp to 6 months after confirming no HTTP-only subdomains break: max-age=15768000; includeSubDomains
Stage 3 — long-term (2 years) with preload readiness: max-age=63072000; includeSubDomains; preload

Never add includeSubDomains until you have audited every subdomain. I have seen marketing sites break an internal http://staging.example.com or an old http://legacy-api.example.com that was not ready for HTTPS. Similarly, do not submit to the HSTS preload list until you are certain you can serve every subdomain over HTTPS indefinitely — removal from the list takes months and requires a browser update.

Apply HSTS only on HTTPS responses. If your CDN serves both HTTP and HTTPS, configure the rule to add the header only when X-Forwarded-Proto or scheme is https, and issue a 301 redirect to HTTPS for all HTTP requests at the edge. A simple edge redirect avoids ever sending HSTS over cleartext, which browsers will ignore anyway per RFC 6797 on the IETF Datatracker.

# Example: Fastly VCL - add HSTS only on HTTPS in vcl_deliver
sub vcl_deliver {
  if (req.http.Fastly-SSL || req.http.X-Forwarded-Proto == "https") {
    if (!resp.http.Strict-Transport-Security) {
      set resp.http.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload";
    }
  } else {
    # Optional: ensure HTTP does not leak HSTS
    unset resp.http.Strict-Transport-Security;
  }
}

Also set your CDN to include HSTS on error responses and redirects. Some CDNs omit custom headers on 301/302 by default. Check that behavior — a redirect without HSTS means the first hop remains vulnerable to SSL stripping on subsequent visits before max-age is set.

Taming Permissions-Policy and Other Response Headers at the CDN Layer

Permissions-Policy (formerly Feature-Policy) controls which browser features can be used by your document and embedded iframes — camera, microphone, geolocation, fullscreen, payment APIs, and more. It is less discussed than CSP or HSTS, but it is valuable for reducing attack surface, especially for sites that embed third-party content.

A restrictive default I apply at the edge for most marketing and documentation sites disables sensitive sensors entirely:

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=()
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site

Note that X-Frame-Options: DENY is now largely superseded by frame-ancestors 'none' in CSP, but I still send both for older browsers. X-Content-Type-Options: nosniff is critical to prevent MIME-sniffing attacks and should be on every response, not just HTML — unlike CSP, this one belongs on all content types.

Scoping Headers by Content Type and Path

Not every header belongs on every response. In my experience, this scoping avoids bloat and compatibility issues:

CSP, Permissions-Policy, Referrer-Policy, Cross-Origin-Opener-Policy — HTML documents only.
HSTS, X-Content-Type-Options — all responses over HTTPS.
Cross-Origin-Resource-Policy — on assets like images, fonts, and APIs that you want to isolate.

At the CDN, implement this with separate response header policies per behavior or with a lightweight edge function that checks Content-Type. For example, with AWS CloudFront you would create one ResponseHeadersPolicy for static assets (HSTS + nosniff only) and another for dynamic/HTML (adds CSP + Permissions-Policy). With Cloudflare or Akamai you would use a rule like: if URI path ends with .html or Content-Type contains text/html, then add CSP.

CDN Configuration Patterns for Header Injection, Inheritance and Cache Variance

Every CDN has its own abstraction for headers, but the underlying patterns are the same. You need to choose between static header policies and programmable edge logic.

Static policies are declarative and fast. CloudFront ResponseHeadersPolicy, Cloudflare Transform Rules (Modify Response Header), and Akamai Property Manager’s “Modify Outgoing Response Header” are examples. They are ideal for HSTS, nosniff, and simple Permissions-Policy values that do not change per request. Use them when the header value is fixed.

Programmable logic is needed when the value must be dynamic — CSP nonces, reporting endpoints per environment, or conditional headers by geography or authentication state. This is where EdgeWorkers, Cloudflare Workers, Fastly Compute, or CloudFront Functions come in. The trade-off is cost and latency: a Worker adds 1-5ms, while a static rule is essentially free.

Inheritance also matters. If your CDN configuration has multiple cache behaviors or rule branches, a header set at the top level may be overridden by a more specific rule that does not include it. I always create a baseline header set that is explicitly included in every behavior, then layer additional headers for HTML. Document your inheritance so a later path addition does not silently drop HSTS.

Finally, consider interaction with other edge security controls. Headers are not a replacement for access control or origin cloaking. For example, Origin Protection: IP Whitelisting, Authenticated Pulls and Origin Cloaking ensures attackers cannot bypass your edge headers by hitting the origin directly. If the origin is reachable publicly, an attacker can fetch responses without any of your CDN-applied protections.

Validating, Monitoring and Rolling Back Header Changes Safely

The most common reason header rollouts fail is lack of validation from the client’s perspective. Testing via curl against origin will not show you what the edge is actually serving to browsers globally.

My rollout checklist for any header change is:

1. Deploy to staging zone or with a header like X-Debug-Policy: 1 to isolate the change.
2. Use curl -I -H "Cache-Control: no-cache" and curl -I (to test HIT and MISS) against multiple PoPs or via a global checker. Inspect raw headers for duplicates, missing semicolons, or blocked characters.
3. Run browser tests with DevTools open — the Console will log CSP violations and blocked resources immediately, and the Security panel will show HSTS status.
4. Monitor CSP reporting for 24-48 hours in Report-Only before enforcing. Watch for unexpected sources like browser extensions, ad scripts, or customer-injected chat widgets.
5. Set a short max-age for HSTS initially so a rollback is quick. HSTS cannot be “unset” quickly if you set a long duration — browsers will remember it until it expires.

For monitoring, add the Report-To header alongside CSP reporting and consider a dedicated collector. Even a simple Cloudflare Worker or AWS Lambda endpoint that logs JSON reports to your observability stack is enough to spot trends. Alert on spikes in CSP violations after a deployment — they often indicate a new script domain was added without updating the policy.

Rollback planning is just as important. Keep the previous CDN configuration version one click away. Since header changes are separate from code deploys, I tag them with a version comment like sec-headers-v2024.11-csp-tighten so anyone can revert without guessing. If you are using infrastructure-as-code, make header policies a distinct resource so a revert does not roll back unrelated CDN settings.

Header Primary Protection Typical Edge Value (Beginner Safe) What Happens If Misconfigured
Content-Security-Policy Mitigates XSS and data injection by whitelisting resource sources default-src 'self'; script-src 'self' https://trusted.cdn.com; frame-ancestors 'none' Legitimate scripts/styles blocked, site appears broken; console shows CSP violations
Strict-Transport-Security Forces HTTPS and prevents SSL-stripping downgrade attacks max-age=63072000; includeSubDomains (HTTPS only) HTTP subdomains become inaccessible; preload can make rollback take months
Permissions-Policy Disables sensitive browser features and third-party iframe capabilities camera=(), microphone=(), geolocation=() Features like camera or payment APIs silently fail inside iframes
X-Content-Type-Options Prevents MIME-sniffing and script execution disguised as other types nosniff Browser may execute malicious files with wrong Content-Type
Referrer-Policy / X-Frame-Options Controls referrer leakage and clickjacking via framing strict-origin-when-cross-origin / DENY Sensitive URLs leaked in Referer header; site can be framed by attacker page

Frequently Asked Questions

Should I set security headers at the origin, the CDN, or both?

Set them in one place — the CDN — and have the CDN strip any duplicates coming from origin. Setting in both places creates duplicate headers and split ownership, which makes debugging harder. Keep origin responsible for application logic and let the edge own the security posture for all responses, including cached and error responses.

Will adding CSP or HSTS at the edge break caching or increase latency?

No. Security headers are response headers appended at delivery time and do not affect the cache key by default on CloudFront, Cloudflare, Fastly, or Akamai. Static header injection adds no measurable latency. Only dynamic generation like per-request CSP nonces via Workers adds a few milliseconds, which is negligible for HTML documents.

How do I safely test a new CSP without breaking production?

Deploy it as Content-Security-Policy-Report-Only first, scoped to HTML responses, and collect reports for 48-72 hours. Review which sources would have been blocked, update the allowlist, then switch to enforcing Content-Security-Policy. In my experience, this report-only phase catches 90% of issues from third-party tags and inline scripts before users are affected.

Is HSTS preload recommended for every site?

Not initially. Start with a short max-age without preload, then increase to 6-12 months with includeSubDomains only after you confirm all subdomains work over HTTPS. Submit to the preload list only when you are committed to HTTPS everywhere long-term. Preload is difficult to undo and can make HTTP-only subdomains unreachable for months.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker