← All posts
How-to

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

  1. X-Content-Type-Options — one line, nothing breaks
  2. Referrer-Policy — one line, nothing breaks
  3. Permissions-Policy — nothing breaks unless you actually use those features
  4. Strict-Transport-Security — safe once you're sure everything is on HTTPS
  5. Content-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.

How-toWeb security