GUIDE · BROWSER BOUNDARY · UPDATED 2026-08-29

The security headers checklist: CSP, HSTS and the five that actually matter

Header scanners will happily grade you on twenty directives. Five of them stop real attack classes. This is what each one actually prevents, what it breaks when you get it wrong, and the rollout order that avoids taking production down.

The five that carry the weight

Header scanners grade you on twenty directives. Five of them stop real attack classes; the rest are mostly hygiene. Ship these first, in this order.

HeaderStopsBreaks if wrong
Content-Security-PolicyXSS payload execution, unexpected third-party script loadsYour own scripts, fonts, analytics, embeds — the highest-risk header to ship blind
Strict-Transport-SecurityDowngrade and cookie-stripping attacks on first hopAny subdomain still on plain HTTP becomes unreachable — and browsers cache the rule
X-Content-Type-OptionsMIME sniffing turning an upload into executable scriptAlmost nothing; safe to ship immediately
Referrer-PolicyLeaking full URLs (with tokens in paths) to third partiesAnalytics attribution loses granularity
Permissions-PolicyThird-party frames silently claiming camera, mic, geolocation, paymentLegitimate embeds that need those features

frame-ancestors replaced X-Frame-Options. If you set both, modern browsers use the CSP directive and ignore the legacy header. Keeping X-Frame-Options around costs nothing for old clients, but do not rely on it as your clickjacking defense.

CSP without taking the site down

CSP is the one that breaks production, because it fails closed: anything you forgot to allow simply stops loading. Never ship it enforcing on the first try.

Step 1 — observe with Report-Only

Content-Security-Policy-Report-Only evaluates the policy and reports violations without blocking anything. Run it in production for at least one full traffic cycle, including a marketing campaign or whatever brings in the unusual embeds.

_headers
Content-Security-Policy-Report-Only: default-src 'self';
  script-src 'self'; style-src 'self';
  report-uri /csp-report

Step 2 — read the violations, then decide

Every violation is one of three things: something legitimate you forgot (add it), something legitimate you should remove anyway (remove it), or an actual injection (you just found a bug). Treat the third case as an incident, not a policy tuning task.

Step 3 — enforce, keeping report-uri

Switch the header name to Content-Security-Policy but keep reporting on. Violations after enforcement are your early warning that a deploy introduced a new dependency.

About 'unsafe-inline': with it in script-src, CSP stops being an XSS defense — an injected inline script executes exactly like your own. Nonces or hashes are the real fix. If a third-party tag forces 'unsafe-inline' on you, that is a vendor cost worth naming out loud.

HSTS: the one you cannot take back quickly

HSTS tells browsers to refuse plain HTTP for your domain, and they cache that instruction for the full max-age. Two ways this bites:

Ramp up: max-age=300 for a day, then 86400 for a week, then 63072000 once nothing has broken. Only then consider preload.

Where to set them

Set headers at exactly one layer. Two layers both setting CSP produces a merged policy that is the intersection of both — usually stricter than either author intended, and painful to debug.

_headers (static hosting)
# Cloudflare Pages / Netlify — one file, applies to every route
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=63072000; includeSubDomains
  Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()
  Content-Security-Policy: default-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'

Verify what is actually served

A header in your config is not a header on the wire. CDNs strip, frameworks override, and edge rules silently win.

check.sh
curl -sI https://example.com/ | grep -iE 'content-security|strict-transport|x-content-type|referrer-policy|permissions-policy'

# Check a deep route too — some setups only cover the root
curl -sI https://example.com/some/deep/path | grep -i content-security

Then open the site in a real browser and read the console. A DOM check will not show you a blocked font or a stripped script; a CSP violation message will.

Generating headers from policy

TJ Sentinel emits this set as part of the browser boundary, in the format your stack consumes — _headers for static hosting, middleware for Express and Next.js, or a Worker response transform. The frame-ancestors and third-party allowances come from the policy file, so the same intent produces the same header wherever the site runs.

Generate a policy for your site →