The six security headers worth setting on every site

Which HTTP response headers actually reduce risk, what each one protects against, the exact values to use, and the mistakes that make a Content-Security-Policy useless.

8 min read

Every response your web server sends begins with a few lines of headers. Browsers read them before the page, and some of them change what the browser will allow the page to do: whether it can be put inside a frame on another site, whether a script injected by an attacker can run, whether the next visit must use HTTPS. They cost nothing to add and nothing to serve. Most sites still do not set them, and a Perimetry audit of a typical small-business site finds three or four missing.

This guide covers the six that matter, with values you can copy. It does not cover the ones that no longer do anything (X-XSS-Protection, Expect-CT, Public-Key-Pins); if you have those, remove them.

1. Strict-Transport-Security

Strict-Transport-Security: max-age=31536000; includeSubDomains

Once a browser has seen this header on your site, it will refuse to connect over plain HTTP for a year, even if a user types http:// or clicks an old link. That closes the window during which a redirect from HTTP to HTTPS can be intercepted on a hostile network. It must be sent over HTTPS to count, and max-age should be at least six months (15 552 000 seconds); Perimetry flags shorter values.

Add includeSubDomains only when every subdomain is served over HTTPS, which should be the case anyway. Add preload only if you then submit the domain to the browsers’ preload list; it is a one-way door, so read the conditions first.

2. Content-Security-Policy

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests

The policy lists where the browser may load each kind of resource from. Its main purpose is to make cross-site scripting useless: an attacker who manages to inject <script> into a page gains nothing if the policy allows scripts only from your own domain and not inline.

It is also the header most often set in a way that protects nothing. Perimetry grades a policy rather than just noting its presence, and marks it weak when:

  • script-src contains 'unsafe-inline' without a nonce or hash. This allows exactly the injected scripts the header exists to block.
  • script-src contains 'unsafe-eval', or a wildcard such as * or https:.
  • object-src is missing, so Flash-era plugins and <object> embeds are not restricted. Set it to 'none'.
  • base-uri is missing, so an injected <base> tag can redirect every relative URL on the page.
  • The header is delivered in report-only mode and nothing enforces it.

Start by deploying the policy as Content-Security-Policy-Report-Only with a report-to endpoint for a week, fix the violations (usually inline scripts and third-party widgets), then switch to enforcing. Astro, Next.js and most frameworks can emit hashes or nonces for the inline scripts they need.

3. X-Frame-Options and frame-ancestors

X-Frame-Options: DENY
Content-Security-Policy: ...; frame-ancestors 'none'

Both say the same thing: nobody may display your page inside a frame. Without them, an attacker can load your site in an invisible frame on their page and trick a logged-in user into clicking buttons they cannot see (clickjacking). frame-ancestors is the modern directive and allows exceptions (frame-ancestors 'self' https://partner.example); X-Frame-Options is kept for very old browsers. Set both.

4. X-Content-Type-Options

X-Content-Type-Options: nosniff

Tells the browser to trust the Content-Type you send and never guess. Without it, a file you serve as text can be interpreted as a script if its contents look like one, which turns an upload feature into a script-injection path. There is no reason not to set it.

5. Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

Controls how much of the current URL is sent to other sites when a visitor follows a link or loads a resource. The default in modern browsers is already this value, but setting it explicitly protects visitors on older ones and documents the decision. If your URLs contain anything private (a reset token, a search query, an account identifier), use no-referrer or same-origin.

6. Permissions-Policy

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()

Lists the browser features your pages (and any frames they contain) may use. For a site that does not need the camera, saying so means that a compromised third-party script cannot ask for it either. List the features you do use with (self).

Where to set them

On the server or the reverse proxy, not in page templates, so that every response (including errors, redirects and static files) carries them:

  • Nginx: add_header in the server block, with always so they are also sent on error pages.
  • Apache: Header always set in the virtual host or .htaccess.
  • Caddy: a header block.
  • Cloudflare: Rules → Transform Rules → Modify Response Header, which works even if the origin does not send them.
  • Node and Next.js: a middleware; the helmet package sets sensible defaults.
  • Astro: security.csp in the configuration emits a hashed CSP; the other headers go in the server that fronts the build.

The CSP is the exception: because it depends on what the page actually loads, it is often set by the application rather than the proxy.

Checking the result

Load your site, open the browser’s developer tools, look at the response headers of the main document. Then look at the console: CSP violations are logged there with the blocked resource and the directive that blocked it. Perimetry’s web check reads the same headers on your main host and on up to five other web-facing subdomains, grades the CSP, and reports cookies set without Secure and HttpOnly and CORS policies that allow any origin with credentials, which are the two neighbouring mistakes.

What the report looks like

Content-Security-Policy allows inline scripts — medium https://www.yourdomain.com sends a policy, but script-src includes 'unsafe-inline' without nonces or hashes, so injected scripts would run. Replace 'unsafe-inline' with per-response nonces or the hashes of your inline scripts, and add object-src 'none'; base-uri 'self'.

A missing header is one finding; a present but weak one is another, so a site that sets everything but sets it badly does not score as if it were fine.