// blog/developer/
Back to Blog
Developer · Published September 2, 2026 · 7 min read · By Toine ·

Update note: Rewritten from experience; replaced Report-To with Reporting-Endpoints, fixed the Observatory address, removed an unsourced statistic and a mislabelled link

HTTP Security Headers Checklist: Six Headers and How to Set Them

HTTP Security Headers Checklist: Six Headers and How to Set Them

Security headers are the cheapest fix in web security. Six lines in a server config tell the browser what your site may load, who may put it in a frame and whether plain HTTP is allowed at all. They do not fix bugs in your code, but a good Content-Security-Policy turns a cross-site scripting bug into a blocked request instead of a stolen session.

When a supplier hands me a new release of a portal to test, the header check is the first thing I run. It takes a minute, it needs no account, and it says a lot about how much attention the rest got. This post lists the headers that matter, what each one stops, the exact lines for the common platforms, and how to test them without breaking the site.

* * *

Start Content-Security-Policy in report-only mode

CSP is the header that does the most and breaks the most. It lists the sources a page may load scripts, styles, images and connections from; anything else is blocked.

A reasonable starting policy: ` Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none' `

Scripts only from your own origin, styles from your origin plus inline, images from anywhere over HTTPS, fonts from your origin, API calls to your origin and one named host, and no site may put yours in a frame.

Do not deploy that as-is. Deploy it as Content-Security-Policy-Report-Only first: ` Content-Security-Policy-Report-Only: default-src 'self'; report-to csp Reporting-Endpoints: csp="/csp-report" `

Report-only logs every violation and blocks nothing. Run it for a week or two, read the reports, and you will find the analytics script, the font CDN, the embedded video and the chat widget you forgot. Add them one by one, then switch to the enforcing header. In my experience a CSP that goes straight to enforcement blocks something the business needed.

Two details that trip people up. X-Frame-Options and frame-ancestors overlap; when both are present, browsers that understand CSP use frame-ancestors and ignore the older header. And report-uri still works everywhere but is deprecated in favour of report-to; send both during the transition if you need the old form for an older browser.

* * *

Five more headers with no downside

These take a line each and break nothing on almost every site.

Strict-Transport-Security ` Strict-Transport-Security: max-age=31536000; includeSubDomains ` Once the browser has seen this over HTTPS it refuses plain HTTP to your domain for a year, so the first request can no longer be downgraded. Leave preload off until every subdomain is on HTTPS and you have submitted the domain to the preload list; preload is very hard to undo.

X-Content-Type-Options ` X-Content-Type-Options: nosniff ` Stops the browser guessing a content type. Without it, a file served as text can be run as JavaScript if it looks enough like it.

X-Frame-Options ` X-Frame-Options: DENY ` No other site may put your page in a frame, which is how clickjacking works. Use SAMEORIGIN if you frame your own pages. Keep it even when CSP has frame-ancestors, for the old browsers.

Referrer-Policy ` Referrer-Policy: strict-origin-when-cross-origin ` Same-origin requests get the full URL, other sites get only your origin, and nothing goes from HTTPS to HTTP. This is the current browser default, but setting it explicitly covers older browsers and stops a token in a URL leaking to whichever site you link to.

Permissions-Policy ` Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=() ` Switches off browser features the site does not use, so an injected script cannot use them either. List what you actually need; an empty pair of brackets means nobody, not even you.

The one I would add after those five is Cross-Origin-Opener-Policy: same-origin, which cuts the link between your page and any window that opened it. Test it first: it breaks some OAuth popups and payment flows that talk back to the opener.

Terminal showing HTTP response headers with security headers highlighted
Terminal showing HTTP response headers with security headers highlighted
* * *

Copy the config for your platform

Vercel (vercel.json): `json { "headers": [ { "source": "/(.*)", "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, { "key": "X-Frame-Options", "value": "DENY" }, { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }, { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }, { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=(), payment=()" } ] } ] } ` In a Next.js project the same entries go in the headers() function of next.config.js instead; the shape is the same list of key and value pairs.

Nginx: `nginx add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; add_header Content-Security-Policy "default-src 'self'" always; ` The always matters: without it Nginx drops the headers on error pages, and the 404 page is the one an attacker looks at.

Apache (.htaccess or the vhost): `apache Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "DENY" Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" `

Cloudflare: Rules, then Transform Rules, then Modify Response Header, one rule per header. Cloudflare also has a managed transform that adds the simple headers in one click; check what it sets, and note that it does not write your CSP.

Whatever the platform, set each header in one place. Two layers that both set the same header is how you end up with a value nobody chose.

* * *

Test from outside, not from your own browser

Your browser has cached your HSTS decision and your CDN may not match your origin, so test with something that fetches fresh.

  • The HTTP Headers Checker on this site fetches a URL, follows the redirects, prints every response header and grades the six security headers pass or fail. I run it on the production URL first, so the baseline is written down before anyone changes anything.
  • securityheaders.com gives a letter grade from A+ to F with a note per header. Good for showing a manager; the grade is a summary, not an audit.
  • Mozilla's HTTP Observatory (now at developer.mozilla.org/en-US/observatory) goes further: headers, cookies and TLS, with a score out of 100 and a reason for each point lost.
  • Your own DevTools, Network tab, any request, Response Headers. This is the truth for that one request, cache and all.

The order I use:

  1. Scan production. Save the result.
  2. Add the five simple headers on staging. Scan again.
  3. Add CSP in report-only. Wait a week. Read the reports.
  4. Fix the legitimate blocks: fonts.googleapis.com and fonts.gstatic.com, the analytics host in both script-src and connect-src, inline styles from a component library.
  5. Switch CSP to enforcing. Scan once more.
  6. Put the scan in the release checklist. A reverse proxy change can drop every header without anyone touching the application, and the only defence is that somebody looks after each release.

If you want to see the raw exchange rather than a verdict, the API Request Builder shows the response headers for any request you send.

Key takeaway

Your browser has cached your HSTS decision and your CDN may not match your origin, so test with something that fetches fresh.

* * *

Use nonces before you use 'unsafe-inline'

Modern front ends ship inline scripts, and 'unsafe-inline' in script-src throws away most of what CSP gives you. Three alternatives, from most to least practical.

Nonces. Generate a random value per response, put it in the header and on each inline script tag: ` Content-Security-Policy: script-src 'nonce-R4nd0mV4lu3' 'strict-dynamic' ` `html ` Scripts without the nonce do not run. 'strict-dynamic' extends trust to scripts that a nonced script loads itself, which is how most bundlers and tag managers work. The nonce has to change on every response, so this does not work on a fully static page served from a CDN.

Hashes. For static pages, hash the content of each inline script and list the hashes: ` Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' ` Change one character in the script and the hash is wrong, which is the point and also the maintenance burden. You can compute it with openssl or the Hash Generator; CSP wants the base64 form, so convert if your tool prints hex.

Reports. Whatever you choose, keep receiving violation reports in production: ` Content-Security-Policy: default-src 'self'; report-to csp Reporting-Endpoints: csp="/csp-report" ` Reporting-Endpoints replaces the older Report-To JSON header. Reports are how you learn that marketing added a script, or that someone is trying to inject one.

Security scanner results showing A+ rating for header configuration
Security scanner results showing A+ rating for header configuration
* * *

FAQ

Will security headers break my website?

The five simple ones almost never do. HSTS is the exception if any subdomain still serves plain HTTP, and preload is the one you cannot take back. CSP breaks things by design; that is what report-only mode is for.

Which header should I add first?

X-Content-Type-Options, then X-Frame-Options, then Referrer-Policy and Permissions-Policy: four lines, no risk. HSTS as soon as every host is on HTTPS. CSP last, after the report-only week.

My CDN and my origin both set headers. Which wins?

Usually the layer closest to the visitor, so the CDN. Set each header in one place only and let the other pass it through; two sources means the next change to either one causes a silent conflict. Then test from outside, because your origin will keep showing you the header the CDN just removed.

Are the headers enough?

No. They limit the damage from a bug; they do not remove it. Input validation, parameterised queries, authentication, session handling and HTTPS are still yours to get right. Headers are the layer that makes a mistake in one of those survivable.

Key takeaway

### Will security headers break my website.