A security header is an HTTP response header that instructs the browser to enforce a protection on your behalf, and setting the right handful is one of the highest-return, lowest-effort things you can do to harden a web app. Most of these headers take a single line of server config, apply to every page at once, and defend against entire attack classes like clickjacking and protocol downgrade. The catch is that a couple of them can break your site if configured carelessly, so this guide separates the safe defaults from the ones that need testing.
The headers that matter, ranked by return
Not every proposed security header earns its place, and some once-recommended ones are now deprecated. Here are the ones worth setting in 2025, roughly in order of value per minute spent.
Strict-Transport-Security (HSTS)
HSTS tells the browser to only ever connect to your site over HTTPS, which shuts down protocol-downgrade and SSL-stripping attacks after the first visit.
Strict-Transport-Security: max-age=31536000; includeSubDomains
The max-age is in seconds; a year is standard. Add includeSubDomains only when every subdomain genuinely serves HTTPS, because it applies to all of them. Add preload and submit to the browser preload list only when you are certain, since removal is slow.
Content-Security-Policy (CSP)
CSP is the most powerful header and the one most likely to break things, which is why it goes second. It restricts where the browser may load scripts, styles, images, and other resources, and a good policy is the strongest defense against cross-site scripting.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'
Start in report-only mode so you learn what a strict policy would block before you enforce it:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
Avoid 'unsafe-inline' and 'unsafe-eval' in script-src; they defeat most of the point. For inline scripts you actually need, use a nonce or hash instead.
X-Content-Type-Options
One value, no downside, set it everywhere. It stops the browser from MIME-sniffing a response into a different content type, which prevents a class of attacks where an uploaded file is interpreted as script.
X-Content-Type-Options: nosniff
Referrer-Policy
Controls how much of the referring URL is sent on outbound requests, limiting leakage of paths and query strings to third parties.
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin and framing controls
frame-ancestors in your CSP is the modern way to prevent clickjacking by controlling who may embed your site in an iframe. It supersedes the older X-Frame-Options header, though setting X-Frame-Options: DENY as well does no harm for older browsers. The Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy headers add isolation for sites that need it.
Headers to stop using
A few headers show up in old hardening guides but should be removed rather than set:
- X-XSS-Protection is deprecated. Modern browsers ignore it, and its legacy filter could itself be abused. Set it to
0or omit it and rely on CSP. - Public-Key-Pins (HPKP) is dead. It caused more outages than it prevented attacks and browsers dropped support. Never enable it.
- Expires and Pragma for security purposes are superseded by proper
Cache-Controldirectives.
Blindly copying a decade-old header list is its own small risk. Fewer, current headers beat a long stale list.
Copy-ready server config
For Nginx, a hardened baseline looks like this:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" always;
The always flag ensures the header is sent even on error responses, which is easy to miss. For an Express app, the helmet middleware sets a sensible default set in one line:
const helmet = require("helmet");
app.use(helmet());
Helmet's defaults are conservative and safe; customize the CSP for your app's real resource origins rather than disabling it.
Verify, do not assume
Setting a header in config does not guarantee it reaches the browser, because a CDN, load balancer, or framework can strip or override it. Check the actual response:
curl -sI https://example.com | grep -i -E 'strict-transport|content-security|x-content-type|referrer'
Free scanners like the Mozilla Observatory and securityheaders.com grade a public site and explain each gap. Wire a header check into CI so a config change that drops a header fails the build rather than silently regressing. Header hygiene is exactly the kind of low-cost misconfiguration a DAST scan surfaces automatically, and the Safeguard Academy has a hardening checklist that goes beyond headers.
Roll out without breaking things
The safe order is: set the no-downside headers first (nosniff, Referrer-Policy, HSTS with a short max-age you ratchet up), then tackle CSP in report-only mode. Collect CSP reports for a week or two, tune the policy until legitimate resources stop showing up as violations, then flip to enforcing. Rushing a strict CSP into enforcement is the number-one way teams break their own site and then abandon the header entirely.
FAQ
What are the most important security headers to set first?
Start with Strict-Transport-Security, X-Content-Type-Options: nosniff, and Referrer-Policy, because they carry protection with essentially no risk of breaking a site. Add Content-Security-Policy next, but roll it out in report-only mode first.
Will a Content-Security-Policy break my site?
It can, if the policy blocks scripts, styles, or resources your pages legitimately load. That is why you deploy CSP in report-only mode first, review the violation reports, and only enforce once legitimate resources no longer trigger them.
Is X-Frame-Options still needed?
The modern replacement is the frame-ancestors directive in your CSP, which is more flexible. Setting X-Frame-Options: DENY alongside it is harmless for older browsers, but new deployments should treat CSP frame-ancestors as the primary clickjacking control.
How do I check which security headers my site sends?
Inspect the real response with curl -I, or use a free grader like securityheaders.com or the Mozilla Observatory. Verify against the live site rather than your config file, because a CDN or proxy can add or strip headers in transit.