HTTP security headers: which seven you actually need

There are about forty HTTP response headers you could set for security. Seven of them carry ninety-five percent of the value. Here's what each one does, how to deploy it, and the common mistakes that turn a strong header into useless decoration.

Every HTTPS website sends a stack of response headers back to the browser with every page load. Most are functional: Content-Type, Cache-Control, Server. Some are for security. The security ones are how you instruct the browser to be paranoid on your behalf — to refuse to embed your page in a frame, to block mixed-content requests, to refuse to downgrade to HTTP.

You can scan any URL on our security headers checker to get a letter grade. The grade is calculated from seven specific headers. Here's what each one does, what a strong value looks like, what a weak value looks like, and how to set it on the three configurations 90% of sites use: Nginx, Apache, and Cloudflare (in front of any origin).

1. Strict-Transport-Security (HSTS) — the single biggest win

If you set only one header on this list, set HSTS. It tells the browser: "next time someone types your domain into the URL bar, even without https://, jump straight to HTTPS and don't even try HTTP."

Why this matters: without HSTS, the first request a user makes to your domain goes over HTTP. An attacker on the same WiFi can intercept that request and either serve a phishing version of your site, or downgrade the connection so it never reaches HTTPS. HSTS closes the window.

Good value:

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

This says: enforce HTTPS for one year (max-age in seconds), for every subdomain, and request inclusion in the HSTS preload list so even browsers that have never visited your site will refuse HTTP.

Weak value: max-age=300 alone. Five minutes of HSTS means an attacker just has to wait for the cookie to expire.

The trap: once you set HSTS with includeSubDomains, EVERY subdomain must support HTTPS. Forever (well, for max-age). If you have an internal admin.example.com on HTTP-only because it's behind a VPN, HSTS will break it for any browser that's seen your main domain. Test on staging first, start with a small max-age like one day, expand to a month, then a year, then add preload.

To get on the official preload list (so Chrome, Firefox, Safari, Edge all ship with your domain pre-enforced): submit at hstspreload.org. Requires max-age=63072000 (2 years), includeSubDomains, preload, and a working redirect from HTTP to HTTPS on the apex.

2. Content-Security-Policy (CSP) — the hardest to get right

CSP is the most powerful header and also the one most likely to break your site. It tells the browser: "only execute scripts from these sources, only load images from these sources, only allow inline styles if they have this nonce."

The full spec has dozens of directives. The minimum useful CSP for a typical site:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

That says: by default, only load resources from your own origin. Scripts must be from your origin (no inline scripts, no eval). Styles from your origin plus inline (most sites need this for legacy reasons). Images from anywhere over HTTPS. No iframe embedding. Forms can only submit to your own origin.

The strong-CSP move: use 'strict-dynamic' with nonces or hashes. Modern frameworks like Next.js, Remix, Nuxt 4+ generate per-request nonces, which means you can use script-src 'strict-dynamic' 'nonce-RANDOM' and never need to maintain an allowlist of script sources. Every script you include gets the nonce; nothing else runs.

The common mistake: setting CSP in "report-only" mode and never moving to enforcement. Content-Security-Policy-Report-Only is great for testing — it logs violations without blocking them. But after a week or a month, you have to switch to Content-Security-Policy to get any actual protection. A surprising number of sites we scan have a strong CSP set as report-only and an empty CSP as the actual one.

3. X-Frame-Options — clickjacking protection

X-Frame-Options stops other sites from embedding yours in an <iframe>. Without it, an attacker can put your login page inside their own page, overlay a UI on top, and trick users into clicking buttons that actually click on the hidden form below.

Good value: X-Frame-Options: DENY. No site, ever, can embed you. Use this unless you have a specific need to be embedded.

Alternative: X-Frame-Options: SAMEORIGIN allows only your own domain to frame you. Use this for sites with internal iframes (like email widget systems).

The replacement: CSP's frame-ancestors directive does the same job and is more flexible (you can allow specific origins). If you set CSP frame-ancestors 'none', you don't technically need X-Frame-Options anymore, but most security scanners still check for both. Set both.

4. X-Content-Type-Options — stop MIME sniffing

Browsers used to do clever "content type sniffing" — if you served a file as text/plain but it looked like JavaScript, the browser would execute it as JavaScript anyway. Helpful for misconfigured 2005 servers. Catastrophic for security.

The only value:

X-Content-Type-Options: nosniff

This tells the browser to trust the Content-Type header and refuse to second-guess it. There is no weak version. There is no nuance. Set it. Done.

5. Referrer-Policy — control what you leak

When a user clicks a link from your site to another site, the browser sends a Referer header (yes, with the historic spelling) saying where they came from. By default, this includes the full URL, including any query parameters. That's a privacy leak — if your URL contains a session token, an order number, or a search query, you're sending that to whoever you linked to.

Best general-purpose value:

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

This sends the full URL when navigating within your own site (useful for analytics), only the origin (no path, no query) when navigating to a different site, and nothing at all when going from HTTPS to HTTP.

For maximum paranoia: Referrer-Policy: no-referrer. The browser never sends a referrer header at all. This breaks some analytics and most affiliate tracking, but it's the most privacy-preserving option.

6. Permissions-Policy — what browser features sites can use

Permissions-Policy (formerly Feature-Policy) lets you declare which browser APIs your site allows or denies. The browser will refuse to give your site access to APIs you've explicitly denied — protecting users if your site is ever compromised.

Good baseline for a static site:

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

That denies access to the camera, microphone, geolocation, payment request API, USB device access, and Google's now-mostly-defunct FLoC interest cohorts.

If your site needs one of these: use self instead of empty parens, like geolocation=(self). The directive is "deny by default, explicitly allow for these origins."

7. Cross-Origin-Opener-Policy (COOP) — protect against Spectre

This one is the newest of the major headers and the one most often missing from older sites. It isolates your browsing context so that pages opened from your domain in other tabs (via window.open) can't read your DOM through side-channel attacks like Spectre.

Standard value:

Cross-Origin-Opener-Policy: same-origin

This is also a prerequisite for enabling SharedArrayBuffer and other high-precision-timer features, which means if your site uses WebAssembly, computer vision, or anything performance-sensitive, you probably want this on.

How to actually set these — three configurations

Nginx (in server or location block)

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;

The always keyword ensures the header is sent even for non-200 responses (404s, 500s). Without it, an attacker can sometimes trigger an error page that bypasses your headers.

Apache (in .htaccess or vhost)

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Content-Security-Policy "default-src 'self'; ..."

Cloudflare (in front of any origin)

The Transform Rules feature lets you add response headers without touching origin. Go to Rules → Transform Rules → Modify Response Header. Add one rule per header above. Takes about 10 minutes.

Alternatively, Cloudflare's Page Rules can set Security Level → High which auto-sets a baseline. But the Transform Rules approach is more explicit and easier to debug when something breaks.

The cardinal sin: setting weak values and forgetting

The most common pattern we see when grading sites: a header is present, so the basic scan passes, but the value is so weak it provides no actual protection. Examples:

  • Strict-Transport-Security: max-age=60 — one minute of HSTS is useless
  • X-Frame-Options: ALLOWALL — explicitly allows what the header was designed to block
  • Content-Security-Policy: default-src * — allows literally anything
  • Referrer-Policy: unsafe-url — sends full URL to everyone, defeats the point

A scan grader that just checks header presence gives these sites an A. Our grader checks values and gives them a C or D. Worth running periodically; we've found regressions on our own site that way.

What we didn't include and why

Headers we deliberately skipped:

  • X-XSS-Protection — deprecated. Was useful in old IE/Edge. Modern Chrome and Firefox removed support. Setting it does nothing in 2026. Some scanners still flag its absence; ignore them.
  • Public-Key-Pins (HPKP) — deprecated everywhere. Using it can permanently break your site if you rotate keys wrong. Never set this header. Use Certificate Transparency monitoring instead.
  • Expect-CT — deprecated. CT enforcement is now built into all major browsers automatically.
  • Feature-Policy — superseded by Permissions-Policy. Set Permissions-Policy instead.

And the always-misunderstood: Server and X-Powered-By. Many guides tell you to remove or fake these. The security benefit is minimal — attackers can fingerprint your stack a dozen other ways. Removing them is fine but it's a "feels secure" move, not actual hardening.

One thing to do today

Run our security headers checker against your own site. The result will be either "you already have all of these" (in which case, well done, you're in the top 5% of public websites) or "you're missing N of the seven." Most sites are missing 3-5. The fix is generally one block in your Nginx config, a single deploy, and a 30-minute test pass to make sure nothing broke.

Then re-run the scan in a week to verify nothing's regressed. Then forget about it. The headers are the rare security control where set-and-forget is actually fine.

Grade your site's security headers — A to F

Free, no signup, no rate limit. Same rubric as securityheaders.com but with an open-source grader.

Run the check →