Read your security headers in one command
Five response headers do most of the browser-side hardening work, and you can see all of them with curl. Here's what to run, what each one does, and what to set if it's missing.
You can check any site's security headers from a terminal. Your own, a supplier's, a thing you're about to integrate with.
The command
curl -sD - -o /dev/null https://example.com
-D - dumps the response headers to stdout, -o /dev/null throws the page away. Use that rather than curl -I, because -I sends a HEAD request and some servers don't attach the same headers to those.
To filter down to the interesting ones:
curl -sD - -o /dev/null https://example.com | grep -iE \
'content-security-policy|strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy'
What you're looking for
Strict-Transport-Security — tells the browser to never use plain HTTP for this domain again. Without it, the very first request can be downgraded.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Start with a short max-age while you're testing. It's hard to undo once browsers have cached it.
Content-Security-Policy — controls where scripts, styles and everything else can load from. The one that does the most and takes the most work.
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Two things people get wrong. 'unsafe-inline' in script-src switches off most of the protection, because injected XSS is almost always inline. And https: as a source isn't a restriction, it matches every host on the internet.
X-Content-Type-Options: nosniff — stops the browser guessing a file's type and treating your uploaded .txt as JavaScript. One line, no downside, set it.
Referrer-Policy — stops your URLs leaking to other sites. If your paths contain ids or tokens, this matters more than it looks.
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy — turns off browser features you don't use.
Permissions-Policy: camera=(), microphone=(), geolocation=()
frame-ancestors — this is a CSP directive, not its own header. It replaces the old X-Frame-Options and stops your page being framed for clickjacking. It's worth calling out separately because it does not fall back to default-src. Neither does base-uri or form-action. If you haven't written those three explicitly, you don't have them, no matter how strict the rest of the policy looks.
Where to set them
Most of the time this is a few lines in your edge config rather than anything in the app. On Netlify, a netlify.toml:
[[headers]]
for = "/*"
[headers.values]
Strict-Transport-Security = "max-age=31536000; includeSubDomains"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
nginx uses add_header, Cloudflare has Transform Rules, and most frameworks have a middleware for it.
Do them in this order
X-Content-Type-Options— one line, nothing breaksReferrer-Policy— one line, nothing breaksPermissions-Policy— nothing breaks unless you actually use those featuresStrict-Transport-Security— safe once you're sure everything is on HTTPSContent-Security-Policy— the real work
The first four are an afternoon at most. CSP is the one that takes iteration, and the way to do it is Content-Security-Policy-Report-Only first, so you can see what would break before anything does. Just make sure you eventually switch the header name over. A policy stuck in report-only mode blocks exactly nothing, and plenty of them have been sitting there for years.
If you want help building one, the CSP builder works from what your site actually loads and names what a policy will break before you ship it. The longer argument about why most policies do nothing is here.