2026-06-16 · 9 min read
Security Headers Explained: HSTS, CSP, X-Frame-Options and More
Security headers are the cheapest hardening you can ship — a few lines in your server config and your site shrugs off a whole class of attacks. This guide walks through HSTS, CSP, X-Frame-Options and the rest from hands-on practice, including what's deprecated and what actually moves the needle. You'll get copy-paste config for nginx, Apache and common platforms, plus how a header checker grades your responses.
I've lost count of how many "secure" sites I've audited that ship a beautiful TLS setup and then forget the one thing that costs nothing: HTTP response headers. You already have a server returning headers on every request. Adding a handful of the right ones is the cheapest hardening you'll ever do — no new dependency, no build step, just config.
So let's go through them the way I do on a real audit: what each header actually buys you, where it bites you, and the exact lines to paste into nginx or Apache.
Why do HTTP response headers matter?
Every response your server sends carries headers, and a subset of them tell the browser how to behave defensively. They don't patch bugs in your code — they reduce the blast radius when something goes wrong (a stray inline script, a framing attack, a downgrade to plain HTTP). The browser is doing the enforcement for you, on the client, for free.
The OWASP Secure Headers Project maintains the canonical list of which headers to set and what to set them to, and the OWASP HTTP Security Response Headers Cheat Sheet is the reference I keep open during config work. The nice part: headers are additive. You can roll them out one at a time and test as you go (which you should, because exactly one of them — CSP — will happily break your site if you're careless).
HSTS: HTTPS enforcement and the preload caveat
Strict-Transport-Security (HSTS) tells the browser: from now on, only ever talk to this host over HTTPS, no exceptions, for the duration I specify. After the first visit, the browser refuses plain-HTTP connections to your domain entirely — which kills the SSL-stripping man-in-the-middle window that a plain redirect leaves open.
max-age is in seconds (here, two years). includeSubDomains extends the policy to every subdomain — powerful, and a foot-gun if you have a subdomain that legitimately can't do HTTPS. preload is the spicy one. Per the OWASP cheat sheet, adding preload and submitting your domain to the browser preload list means browsers enforce HTTPS before the first request — but removal from that list is slow and painful, so only opt in once you're certain every subdomain will serve HTTPS indefinitely. I treat preload as a one-way door: don't walk through it on a domain you're still experimenting with.
CSP: powerful, but easy to break
Content-Security-Policy is the heavyweight. It tells the browser which sources of script, style, images, frames and so on are allowed to load, which is your strongest defense against cross-site scripting (XSS). It's also the header most likely to white-screen your site if you get it wrong (ask me how I know).
Per MDN's Content-Security-Policy reference, a policy is a set of directives, each naming a resource type and its allowed sources:
The trap is inline scripts and styles. A strict policy blocks <script>...</script> and onclick="..." attributes outright, which breaks a lot of legacy markup and many third-party widgets. MDN and web.dev's security headers guide both recommend the nonce/hash approach over the blunt 'unsafe-inline': generate a per-response nonce, attach it to your trusted inline scripts, and reference it in the policy. The way I always deploy CSP is in report-only mode first:
This logs violations without enforcing anything, so you can watch real traffic, see what you'd break, and tighten the real policy before flipping it on. Skipping this step is how you ship an outage.
X-Frame-Options and clickjacking
Clickjacking is when an attacker loads your page in an invisible iframe over their own UI and tricks a logged-in user into clicking something they didn't mean to. X-Frame-Options stops it by controlling whether your page can be framed at all:
DENY blocks all framing; SAMEORIGIN allows framing only by your own origin. The OWASP cheat sheet notes this header is effectively superseded by CSP's frame-ancestors directive, which is more flexible (it accepts a source list, not just three keywords). In practice I set both: frame-ancestors for modern browsers and X-Frame-Options as a belt-and-suspenders fallback. They cost nothing together.
X-Content-Type-Options, Referrer-Policy, Permissions-Policy
Three smaller headers that show up on every checklist:
- X-Content-Type-Options: nosniff — stops the browser from MIME-sniffing a response into a type other than the declared
Content-Type. Per the OWASP cheat sheet, this blocks attacks where, say, an uploaded "image" gets interpreted and executed as a script. One value,nosniff, and you're done. - Referrer-Policy — controls how much of the referring URL leaks to other sites when a user clicks a link or loads a resource.
strict-origin-when-cross-originis the sane default the OWASP cheat sheet points to: full URL for same-origin requests, only the origin for cross-origin, and nothing on an HTTPS-to-HTTP downgrade. - Permissions-Policy — the successor to Feature-Policy, it lets you disable browser features (camera, microphone, geolocation) for your page and embedded frames. The OWASP Secure Headers Project recommends explicitly turning off what you don't use:
What's deprecated: X-XSS-Protection, Expect-CT, HPKP
This is where I save you from old blog posts. Three headers you'll still see recommended in stale tutorials, but shouldn't deploy:
- X-XSS-Protection — controlled a legacy browser XSS auditor that modern browsers have removed. The OWASP cheat sheet advises against relying on it; a good CSP replaces it entirely. Some guides even set it to
0to explicitly disable buggy behavior. - Expect-CT — enforced Certificate Transparency reporting. It's obsolete now that CT is enforced by default in major browsers, and MDN and OWASP both treat it as deprecated.
- HPKP (Public-Key-Pins) — HTTP Public Key Pinning was removed from browsers. It was a fantastic way to brick your own domain if you rotated a key wrong, so its retirement is no loss. The OWASP Secure Headers Project lists it as deprecated.
If a "security header" tutorial still tells you to set HPKP, close the tab.
How a header checker grades your headers
A scanner like the one on hostingchecker.org makes a request to your URL, reads the response headers, and checks them against a recommended baseline — essentially the OWASP Secure Headers Project list. Each present, well-configured header earns credit; each missing or weak one (say, an HSTS with a tiny max-age, or no CSP at all) costs you. The output is a grade plus a per-header breakdown of what's missing and what to add.
Two things I always remind people about checkers. First, a high grade is necessary but not sufficient — a perfect CSP score on a policy full of 'unsafe-inline' is theater. Second, headers can differ per path and per response type, so test a few representative URLs, not just your homepage.
Fix examples: nginx, Apache and common platforms
Here's the baseline I deploy. Adjust CSP to your actual asset sources — it's the only one that needs per-site tuning.
nginx (inside server {}):
The always flag matters: without it, nginx skips add_header on error responses (4xx/5xx), leaving those pages unprotected.
Apache (in your vhost or .htaccess, with mod_headers enabled):
Common platforms. On WordPress without server access, a security plugin (or a small add_filter on wp_headers) can inject these. On a Cloudflare-fronted site, the Transform Rules / Managed Headers feature sets them at the edge. On static hosts like Netlify, a _headers file does the job. The values stay identical to the snippets above — only the place you put them changes. As always, ship CSP in report-only first (Content-Security-Policy-Report-Only) and watch your reports before enforcing, as web.dev recommends.
FAQ
Will adding security headers slow down my site?
No. Headers add a few bytes per response and require no extra round trips. The only "cost" is the engineering time to get CSP right.
Which single header should I add first if I can only do one?
HSTS, assuming you're already fully on HTTPS. It's a one-liner with no risk of breaking page content, and per the OWASP cheat sheet it closes the HTTPS-downgrade window. CSP is more powerful but needs careful tuning.
Why is my CSP blocking my own scripts?
Almost always inline <script> blocks or on* attributes, or an asset loaded from a CDN you didn't list. Per MDN, use nonces or hashes for trusted inline scripts and add legitimate external origins to script-src — avoid reaching for 'unsafe-inline'.
Do I still need X-Frame-Options if I set CSP frame-ancestors?
The OWASP cheat sheet treats frame-ancestors as the modern replacement, but setting both costs nothing and covers older clients. I keep both.
Is X-XSS-Protection worth setting?
No. The OWASP cheat sheet considers it legacy; a solid CSP is the real XSS defense. Same goes for Expect-CT and HPKP — both deprecated.
How often should I re-test my headers?
Any time you change your server config, add a third-party widget, or migrate platforms — those are exactly the moments a header silently disappears. A quick scan after each deploy catches regressions.
Headers are the rare security win that's almost free. Spend an afternoon, ship the baseline, tune your CSP in report-only mode, and then check your security headers to confirm the grade.
