How to add security headers on Netlify
Six HTTP response headers cover the standard hardening checklist. Below: what each one does (honestly — one of them is already the browser default), then the exact Netlify configuration.
The headers, and what they actually buy you
- Strict-Transport-Security — max-age=31536000; includeSubDomains
- Tells browsers to only ever use HTTPS for this domain (for 1 year). ⚠ includeSubDomains applies the rule to EVERY subdomain — any HTTP-only subdomain (an old intranet host, a device panel) becomes unreachable until the max-age expires; drop that token if you have one. Only meaningful on HTTPS responses — browsers ignore it over HTTP (RFC 6797). Add the preload token only deliberately: preloading is hard to undo (hstspreload.org).
- X-Content-Type-Options — nosniff
- Stops browsers from guessing content types — the only valid value.
- X-Frame-Options — DENY
- Blocks your site from being framed (clickjacking). The modern replacement is CSP frame-ancestors; keeping X-Frame-Options alongside covers older browsers. Use SAMEORIGIN instead if your site frames itself.
- Referrer-Policy — strict-origin-when-cross-origin
- This is already the browser default — setting it makes your policy explicit rather than fixing a hole. Use no-referrer for the strictest option.
- Permissions-Policy — camera=(), microphone=(), geolocation=()
- Disables powerful browser features your site doesn't use. Minimal sane starting set; extend with more features as needed.
- Content-Security-Policy — (not copy-pasteable — start with Content-Security-Policy-Report-Only)
- CSP is site-specific: a canned policy breaks any third-party script or style you load. Start with Content-Security-Policy-Report-Only to observe violations without breaking anything, then tighten into an enforced policy.
On Netlify
_headers file (in your publish directory)
/* Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=()
netlify.toml (repo root)
[[headers]]
for = "/*"
[headers.values]
Strict-Transport-Security = "max-age=31536000; includeSubDomains"
X-Content-Type-Options = "nosniff"
X-Frame-Options = "DENY"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"Netlify gotchas worth knowing
- ⚠Netlify's own docs: custom headers apply only to files Netlify serves from its store — responses from functions or server-side-rendered pages do NOT get them; set headers in the function/SSR response itself for those.
- ⚠The _headers file must end up in the publish directory — some static-site generators strip underscore-prefixed files from output.
Verified against Netlify: custom headers on 2026-07-12.
Check what your site sends today
The full domain report includes your live response headers, so you can verify the change took effect.
Run a domain report →