Security headers are a handful of lines in your server config that tell the browser how to treat your pages: only load over HTTPS, don't let other sites frame them, don't guess file types, don't leak full URLs to third parties. They cost nothing to send, and when one is missing it shows up straight away in any audit. This guide covers which headers matter in 2026, which ones to drop, and gives you configs you can paste into Nginx, Apache or Cloudflare.
Key takeaways
- Send HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy and a starter Content Security Policy.
- Remove X-Powered-By, X-XSS-Protection, Expect-CT and Public-Key-Pins, as they leak details or are obsolete.
- In Nginx, add the always parameter so 404 and 500 error pages also carry your security headers.
- Roll out a strict Content Security Policy in report-only mode first, because it is the easiest header to break a site with.
The short list
Here is the baseline set, based on the OWASP HTTP Headers Cheat Sheet and MDN:
| Header | Recommended value | What it protects against |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Downgrade to HTTP, cookie theft on public Wi-Fi |
Content-Security-Policy |
Site-specific (start with frame-ancestors, object-src, base-uri) |
Cross-site scripting, injected scripts, framing |
X-Content-Type-Options |
nosniff |
MIME-type sniffing turning uploads into scripts |
X-Frame-Options |
DENY or SAMEORIGIN |
Clickjacking in older browsers |
Referrer-Policy |
strict-origin-when-cross-origin |
Leaking full URLs (tokens, search terms) to other sites |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Third-party scripts using powerful browser features |
Cross-Origin-Opener-Policy |
same-origin |
Cross-window attacks via window.opener |
And the headers you should remove or stop sending:
X-Powered-Byand detailedServervalues (they advertise your stack and version).X-XSS-Protection. OWASP recommends not setting it, or setting it to0. The old browser XSS auditors it controlled have been removed, and the header itself could introduce problems. A Content Security Policy is the replacement.Expect-CTandPublic-Key-Pins. Both are obsolete and unsupported by modern browsers.
What each header does
Strict-Transport-Security (HSTS)
HSTS tells the browser to use HTTPS for your domain for max-age seconds, and to refuse click-through on certificate errors. Browsers ignore it when it arrives over plain HTTP, so it only takes effect after one successful HTTPS visit. Start with a short max-age (say, 300 seconds) while you confirm every subdomain works over HTTPS, then raise it to a year. The preload directive is a bigger commitment. Our HSTS guide covers it in detail.
Content-Security-Policy (CSP)
CSP is the most powerful header on the list and the easiest to break a site with. It controls where scripts, styles, images, frames and form submissions can come from. Don't paste a strict policy straight into production. Roll it out in report-only mode first. Our Content Security Policy guide walks through that process. A safe starter policy that rarely breaks anything:
Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests
This blocks other sites from framing you, disables plugins like <object> and <embed>, stops injected <base> tags from rewriting your relative URLs, and upgrades stray http:// resource requests.
X-Content-Type-Options
nosniff stops the browser from second-guessing the Content-Type you send. Without it, a file uploaded as an "image" that actually contains JavaScript could be run as a script. Make sure your server sends correct content types (and charset=UTF-8 on HTML) before you rely on it.
X-Frame-Options
This is the legacy anti-framing header. CSP frame-ancestors supersedes it, and when both are present, browsers that support CSP ignore X-Frame-Options. It's still worth sending a matching value for older clients. See Clickjacking protection for the differences.
Referrer-Policy
When a visitor clicks a link off your site, the browser sends a Referer header. strict-origin-when-cross-origin sends the full URL to your own pages, only the origin (https://example.com/) to other HTTPS sites, and nothing when going from HTTPS to HTTP. Modern browsers already default to this. Setting it explicitly locks in the behavior and stops audits from flagging it as missing.
Permissions-Policy
This header switches off browser features your site doesn't use, so a compromised third-party script can't use them either. The empty list () means "nobody, not even this site":
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
If you embed a map that needs location, allow just your own origin: geolocation=(self).
Cross-Origin-Opener-Policy
same-origin puts your page in its own browsing context group, so a cross-origin window that opened yours (or that you opened) can't keep a reference to it. Test it if you use OAuth pop-ups or payment pop-ups that talk back to your page through window.opener. Those may need same-origin-allow-popups.
Nginx configuration
Put these in your server block for the HTTPS site:
server {
listen 443 ssl;
http2 on;
server_name example.com;
# Hide version number in the Server header
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "frame-ancestors 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# If PHP or an upstream app adds X-Powered-By, strip it
fastcgi_hide_header X-Powered-By;
proxy_hide_header X-Powered-By;
}
Two Nginx gotchas catch almost everyone:
- The
alwaysparameter. Without it, Nginx only adds the header to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses. Your 404 and 500 pages go out without protection. - Inheritance.
add_headerdirectives are inherited from the enclosing level only if the current level defines noadd_headerof its own. If alocationblock adds a singleCache-Controlheader withadd_header, every security header from theserverblock disappears for that location. The fix is to put the headers in a snippet file andincludeit in each location that usesadd_header.
# /etc/nginx/snippets/security-headers.conf holds the add_header lines above
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
include snippets/security-headers.conf;
}
On Nginx 1.29.3 or newer you can use add_header_inherit merge; instead, which appends the parent level's headers to the ones a location defines. Most distribution packages still ship older versions, so check nginx -v before relying on it.
Test and reload with nginx -t && systemctl reload nginx.
Apache configuration
Enable mod_headers (a2enmod headers on Debian/Ubuntu), then add this to your HTTPS virtual host or .htaccess:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Content-Security-Policy "frame-ancestors 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
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 unset X-Powered-By
Header unset X-Powered-By
</IfModule>
In the main server config (not .htaccess), reduce the Server header:
ServerTokens Prod
ServerSignature Off
Header always set covers error responses too. The duplicate unset lines are deliberate: Apache keeps two header tables, and a header set by PHP can land in either one. For PHP itself, also set expose_php = Off in php.ini.
Cloudflare configuration
If your site sits behind Cloudflare, you can add headers at the edge without touching the origin:
- HSTS and nosniff: In the dashboard, go to SSL/TLS > Edge Certificates > HTTP Strict Transport Security (HSTS). Enable it, choose a max age, and turn on the No-Sniff header option, which sends
X-Content-Type-Options: nosniff. - Everything else: Go to Rules and create a Response Header Transform Rule. Set the filter to "All incoming requests" (or your hostname), then add one Set static operation per header:
Content-Security-Policy,X-Frame-Options,Referrer-Policy,Permissions-Policy,Cross-Origin-Opener-Policy. - Removing headers: In the same rule type, use the Remove operation for
X-Powered-By. Cloudflare doesn't let you change its ownServerheader.
"Set" overwrites any value your origin sends, while "Add" appends a second copy. For security headers you almost always want "Set". Two CSP headers are both enforced, which can block more than you expect.
Verify your headers
From a terminal:
curl -sI https://example.com/ | grep -iE "strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy|cross-origin"
curl -sI https://example.com/this-page-does-not-exist | grep -i strict-transport
The second command checks that your 404 page carries the headers too. Also check a few deep URLs and your static assets, especially if you use the Nginx location blocks described above.
Frequently asked questions
Do security headers improve SEO?
Not directly. Google has not listed security headers as a ranking factor, apart from HTTPS itself. They do protect your visitors and your reputation, and a hacked site loaded with spam will definitely hurt your search visibility.
Should I set headers in the application or the web server?
Static headers like HSTS, nosniff and Referrer-Policy belong at the web server or CDN, where they cover every response. A CSP with nonces has to come from the application, because the nonce changes on every request. Pick one place for each header so you don't send duplicates.
Is X-XSS-Protection still needed?
No. Modern browsers have removed the XSS filter it controlled, and OWASP recommends leaving it out or setting it to 0. Put the effort into a Content Security Policy instead.
Why does my scan still show a header as missing after I added it?
Usual causes are a cached response from a CDN, the Nginx inheritance problem described above, or the header only being added on one virtual host (for example www but not the apex domain). Purge the CDN cache and test each hostname with curl -I.
What to do next
Add the baseline headers above, reload your server, and confirm them with curl on the homepage, a 404 page and a static file. Then run a free audit to see which headers are still missing, along with your TLS certificate and exposed-file checks, in one report. Once the basics are in place, the next step is a proper Content Security Policy.
Related reading
- HTTP/3 and QUIC Explained: Will It Make Your Site Faster?: HTTP/3 and QUIC explained: when they make a website faster, browser and CDN support, and how to enable and test HTTP/3 on Cloudflare, LiteSpeed and Nginx.
Sources
More guides for your industry
Free checklist (PDF)
Free law firms website checklist
30 practical checks for law firms: website speed and mobile, Google Business Profile and reviews, email security, booking and enquiry handling, and AI search visibility. Each one says why it matters, and many link to a free tool that checks it.
Written by the MakeItFlow team
We build the website audit these guides are based on. Every fact is checked against the primary sources listed above . How we research and check
Check your own site in 30 seconds
Run a free audit covering speed, SEO, DNS, email authentication and security headers — and get a prioritised fix list.