GuideTechnical SEO

Security headers checklist: the headers every site should send

The six security headers every site should send, the value for each, the Mozilla Observatory penalty for a missing one, and how to set them on any host.

LaunchScalerPublished 9 min read

Every site should send six security headers: Strict-Transport-Security, Content-Security-Policy, X-Frame-Options (or a CSP frame-ancestors directive), X-Content-Type-Options: nosniff, Referrer-Policy and Permissions-Policy. Mozilla's HTTP Observatory, the free grader MDN runs, deducts 25 points for a missing CSP, 20 for missing HSTS, 20 for missing clickjacking protection and 5 for missing nosniff, from a starting score of 100.

Below is the value to send for each, what the Observatory charges for leaving it out, how to set all of them on Next.js, Vercel, Netlify and Nginx, and a guide for each header that needs more than one line.

Which security headers should every site send?

Send the six headers in the table on every HTML response, over HTTPS, and put an HTTP to HTTPS redirect in front of them. The values below are safe defaults for a typical marketing site or SaaS app; the CSP is the one header you must tailor, because it lists where your scripts come from.

HeaderValue to sendWhat it stopsObservatory if missing or weak
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDowngrades to plain HTTP and cookie theft on the first hopMissing: -20. max-age under six months (15768000): -10
Content-Security-PolicyStart from default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' and add your sourcesInjected scripts (XSS) and unwanted embedsMissing: -25. Report-only header alone: -25. 'unsafe-inline' in script-src: -20. 'unsafe-eval': -10
X-Frame-Options or CSP frame-ancestorsDENY (or frame-ancestors 'none')Clickjacking: your page framed inside someone else'sMissing: -20. Set: +5
X-Content-Type-OptionsnosniffBrowsers guessing a file's type and running it as scriptMissing: -5
Referrer-Policystrict-origin-when-cross-originFull URLs (with tokens or IDs in the query) leaking to other sitesMissing: 0. Private value: +5. unsafe-url, origin, origin-when-cross-origin or no-referrer-when-downgrade: -5
Permissions-Policycamera=(), microphone=(), geolocation=()Embedded third parties using powerful browser APIsNot scored by the Observatory
HTTP to HTTPS redirect301 or 308 to https:// on the same hostVisitors staying on plain HTTPMissing: -20

The penalties come from the Observatory's own scoring chart, in the source code MDN publishes. Two optional headers earn bonus points there too: Cross-Origin-Opener-Policy: same-origin and a same-origin or same-site Cross-Origin-Resource-Policy are each worth +10. The redirect row matters as much as any header, because HSTS only takes effect once a browser has reached you over HTTPS; the HTTP to HTTPS redirect guide sets it up as one hop on each host.

How does Mozilla Observatory score security headers?

Every site starts at 100. The Observatory subtracts penalties first; only if the result is 90 or more does it add bonuses, so extra credit cannot rescue a site with a missing CSP. The highest possible score is 145 and the lowest is 0, and the score maps to a letter grade.

ScoreGrade
100 and upA+
90 to 99A
70 to 79B
50 to 59C
30 to 39D
0 to 24F

The bands in between carry plus and minus grades (85 to 89 is A-, for example). A site with none of the first four headers in the table loses 25, 20, 20 and 5 points, plus 20 more if it does not redirect to HTTPS, which would leave it at 10: an F. Adding those four headers, with a clean CSP, and the redirect brings the same site back to 100 before any bonus.

MDN is candid that the weights are a judgement call. Its scoring page says the grades are "essentially arbitrary" but based on feedback from industry professionals, and that a header one site needs may be unnecessary for another. Use the score to find what is missing, not as a security rating.

What should Strict-Transport-Security be set to?

Send Strict-Transport-Security: max-age=31536000; includeSubDomains once every subdomain works over HTTPS. The Observatory wants at least 15768000 seconds (six months), the preload list wants at least 31536000 (one year), and browsers ignore the header entirely when it arrives over plain HTTP.

Start lower if you are not sure every subdomain serves HTTPS, because includeSubDomains makes the browser refuse plain HTTP on all of them. Preloading is a separate, hard-to-reverse step. The HSTS header guide gives the value for each stage, testing to production to preload, and the removal risk.

What should your Content-Security-Policy be?

Your CSP should allow only the script, style, image and frame sources your pages actually use, block plugins with object-src 'none', and avoid 'unsafe-inline' in script-src. It is the header that needs the most care, because a policy that is too strict breaks the page and one that is too loose protects nothing.

Roll it out as Content-Security-Policy-Report-Only first and read the violation reports, then switch to the enforcing header. Note that the Observatory scores a report-only policy with no enforcing header the same as no CSP at all (-25). The Content-Security-Policy starter guide has a nonce-based starter policy, the rollout steps and the Next.js setup.

X-Frame-Options or CSP frame-ancestors?

Send both: X-Frame-Options: DENY for older browsers and frame-ancestors 'none' inside your CSP for current ones. MDN points to frame-ancestors for anything beyond deny or same-origin, and the Observatory gives the same +5 for either. The one value to avoid is ALLOW-FROM, which modern browsers ignore.

Use SAMEORIGIN (or frame-ancestors 'self') if your own pages embed each other, and list specific origins in frame-ancestors if a partner must embed you. Neither works from a <meta> tag: MDN says X-Frame-Options in a meta element has no effect, and frame-ancestors is not supported there either. They must be real response headers.

What does X-Content-Type-Options: nosniff do?

It tells the browser to trust the Content-Type your server sends instead of guessing from the file's contents. That stops an uploaded file served as text/plain from being run as JavaScript. The only valid value is nosniff, and the Observatory takes 5 points when it is missing.

Send it on every response, not only HTML: scripts, stylesheets and API responses too. Once it is on, a script served with the wrong type is blocked rather than run, so check that your server labels .js files with a JavaScript MIME type and .css files as text/css.

Which Referrer-Policy and Permissions-Policy values should you use?

Use Referrer-Policy: strict-origin-when-cross-origin, which sends the full URL to your own pages, only the origin to other sites and nothing on an HTTPS to HTTP downgrade. For Permissions-Policy, deny every powerful feature you do not use with an empty allowlist, such as camera=(), microphone=(), geolocation=().

Two details here catch people out:

  • strict-origin-when-cross-origin is already the browser default when no policy is set, so the header mostly documents intent and earns the Observatory's +5. What matters is not sending a weaker value. The Next.js headers docs use origin-when-cross-origin as their example, which the Observatory scores as unsafe (-5), so copy the value, not the example.
  • MDN marks Permissions-Policy as not Baseline, because it does not work in some widely used browsers. Send it anyway: it costs nothing where it is ignored and restricts embedded third parties where it is supported. An empty allowlist () disables the feature for your page and every iframe in it.

Which security headers should you remove?

Remove headers that are obsolete or that only help an attacker. OWASP's HTTP Headers Cheat Sheet says not to set X-XSS-Protection (or to set it to 0) and to rely on CSP instead, and to remove Expect-CT and Public-Key-Pins. It also says to remove Server and X-Powered-By or give them non-informative values.

Version strings are the practical one. A header like Server: nginx/1.18.0 or X-Powered-By: Express tells anyone scanning which known vulnerabilities to try. In Nginx, server_tokens off; stops the version appearing in the Server header and on error pages; in Express, app.disable('x-powered-by') drops the header, though Express's own docs note that a determined attacker can still identify the framework.

How do you set security headers in Next.js, Vercel, Netlify and Nginx?

Set them once, at the layer that serves every response: the framework config for Next.js, vercel.json for other frameworks on Vercel, a _headers file on Netlify, and add_header ... always in Nginx. Then check an HTML page, a static asset and a 404, because each platform has a case where headers silently go missing.

Next.js, in next.config.js. Next.js checks these headers before the filesystem, so they also apply to files in /public:

const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
]

module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  },
}

Put the CSP here too if it has no nonce. A nonce-based CSP has to be generated per request in proxy.ts, which the CSP guide covers.

Vercel, in vercel.json for frameworks without their own header config:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Vercel already redirects HTTP to HTTPS with a 308 and sends Strict-Transport-Security: max-age=63072000; on custom domains by default, for that host only. Add your own Strict-Transport-Security header if you want includeSubDomains.

Netlify, in a _headers file in your publish directory:

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Strict-Transport-Security: max-age=31536000; includeSubDomains

Netlify's docs say custom headers do not apply to proxied content or to responses from functions and edge functions, which includes server-rendered pages. Those responses must set their own headers.

Nginx, inside the server block that serves HTTPS:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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=()" always;

Without always, Nginx adds the header only to responses such as 200, 204, 301, 302 and 304, so your error pages go out bare. And a location block with even one add_header of its own inherits none of the ones above it, so check it first when headers vanish on one path.

How do you check your security headers?

Request the page and read the response headers. One command covers the six:

curl -sI https://yoursite.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"

Repeat it for a static file (/favicon.ico) and a URL that returns 404, then run the site through Mozilla's HTTP Observatory at developer.mozilla.org/en-US/observatory for the scored view. The comparison of website security scanners covers what the other free graders read and what none of them can see. In the browser, the Network panel in DevTools shows each response's headers when you click the document request.

Headers are one layer of a site's security. The rest of the checklist lives in these guides:

Check every header in one scan

To check all of this on a live site at once, run the free scan on LaunchScaler. It needs only your URL and no account, and its 29 security checks read HSTS and its preload eligibility, the CSP and any 'unsafe-inline' or 'unsafe-eval', clickjacking protection, nosniff, Referrer-Policy, Permissions-Policy, cookie flags, CORS, the HTTP to HTTPS redirect, TLS versions and the certificate chain, version-leaking Server headers, exposed /.env and /.git, and the SPF, DKIM, DMARC and CAA records in your DNS. They run with 156 checks across 6 of its 7 categories, all free.

Questions, answered

What people ask about this

01

What security headers should a website have?

  • At minimum: Strict-Transport-Security, Content-Security-Policy, X-Frame-Options or a CSP frame-ancestors directive, X-Content-Type-Options: nosniff, Referrer-Policy and Permissions-Policy. Together they force HTTPS, limit which scripts run, block framing and stop MIME sniffing.
02

How do I check my security headers?

  • Run curl -sI https://yoursite.com and read the response headers, or test the URL in Mozilla's HTTP Observatory, which scores each header from a baseline of 100. Check an HTML page and an error page, since some servers drop headers on 404s.
03

What does OWASP recommend for security headers?

  • OWASP's HTTP Headers Cheat Sheet recommends X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Strict-Transport-Security: max-age=63072000; includeSubDomains; preload, a Content-Security-Policy and a Permissions-Policy. It also says not to set X-XSS-Protection, or to set it to 0.
04

Is X-XSS-Protection still needed?

  • No. OWASP says not to set it or to turn it off with X-XSS-Protection: 0, and to rely on Content-Security-Policy instead.
05

Do security headers affect SEO?

  • Not directly as ranking signals. HTTPS and a working redirect matter for how Google indexes the site, and the headers protect the visitors and accounts that search traffic brings you.

Sources

  1. MDN HTTP Observatory: Tests and scoringdeveloper.mozilla.org
  2. MDN HTTP Observatory source: scoring chart (charts.js)github.com
  3. OWASP: HTTP Headers Cheat Sheetcheatsheetseries.owasp.org
  4. MDN: Strict-Transport-Securitydeveloper.mozilla.org
  5. MDN: X-Frame-Optionsdeveloper.mozilla.org
  6. MDN: CSP frame-ancestorsdeveloper.mozilla.org
  7. MDN: Referrer-Policydeveloper.mozilla.org
  8. MDN: Permissions-Policydeveloper.mozilla.org
  9. Next.js: headers in next.config.jsnextjs.org
  10. Vercel: Encryption and TLSvercel.com
  11. Vercel: vercel.json headersvercel.com
  12. Netlify: Custom headersdocs.netlify.com
  13. nginx: ngx_http_core_module (server_tokens)nginx.org
  14. Express: Security best practicesexpressjs.com
  15. nginx: ngx_http_headers_modulenginx.org

Newsletter

What launched, every Monday

Every Monday: what launched on LaunchScaler that week, what is launching next, and the reviews we published. Confirm your email once and you're in.

More from the blog

Guide

SoftwareApplication schema for SaaS: JSON-LD Google accepts

Google's software app rich result needs name, offers.price and a real rating or review. A complete SaaS JSON-LD example and the rating rules.

LaunchScalerSep 28, 2026 · 8 min read
Guide

SPF, DKIM and DMARC for a startup domain: the exact records

Copyable SPF, DKIM and DMARC records for Google Workspace, Microsoft 365 and sending services, and the order to tighten DMARC to reject safely.

LaunchScalerSep 28, 2026 · 8 min read
Guide

Structured data errors: fix 'Missing field' and invalid JSON

A structured data error (critical issue) blocks the rich result; a warning doesn't. How to fix missing fields, invalid JSON and wrong value types.

LaunchScalerSep 28, 2026 · 10 min read