Security headers are the cheapest hardening you will ever ship.
Most security work is expensive: audits, dependency upgrades, threat models, penetration tests. Security headers are the opposite. They are a handful of lines in your server config that tell the browser how to defend your users, they ship in one deploy, and visitors never see them. For the effort involved, nothing else in web security comes close on return.
The reason they get skipped is that they are invisible. A missing Content-Security-Policy does not throw an error, slow your page, or show up in any dashboard your users see. The site looks fine right up until someone injects a script through a comment field, frames your login page to steal credentials, or downgrades a visitor from HTTPS to HTTP on hostile WiFi. This post walks the headers that actually matter, what each one defends against, and exactly how to set them in Next.js, nginx, and at the CDN.
What's in this post
- Why headers are the highest-leverage hardening you can do
- Content-Security-Policy: the most powerful and the hardest
- HSTS and the headers that are safe to ship today
- Permissions-Policy, Referrer-Policy, and clickjacking
- Where to set them: Next.js, nginx, and the CDN
- Grading your coverage in one scan
Why headers are the highest-leverage hardening you can do
A security header is a directive your server sends in every HTTP response that tells the browser to behave more safely: which sources may load scripts, whether the page may be framed, whether to force HTTPS, which browser features the page is allowed to use. They are pure server-side configuration. There is no client code to write, no library to install, and nothing for a visitor to notice.
That combination, invisible to users and a few lines to add, is what makes them among the highest-impact, lowest-effort security improvements available. The browser already knows how to enforce all of these protections. It just will not do anything you do not tell it to do. Sending the right headers is how you turn those defenses on.
The SEO angle is indirect but real. Google does not rank you higher for sending security headers, and anyone who tells you otherwise is guessing. But a compromised site does get punished: defacement, injected spam links, and malware flags tank rankings fast, and recovering a site flagged in Google Safe Browsing is far more expensive than the deploy that would have prevented it. HSTS also reinforces the HTTPS that Google does lightly reward. Treat headers as insurance against the catastrophic losses, not as a ranking lever.
Content-Security-Policy: the most powerful and the hardest
Content-Security-Policy (CSP) is the single most powerful header and, not coincidentally, the hardest to get right. It restricts where scripts, styles, images, fonts, and other resources are allowed to load from. Done well, it is the strongest defense the browser offers against cross-site scripting: even if an attacker injects a <script> tag, the browser refuses to run it because the source is not on your allowlist. CSP also handles clickjacking through the frame-ancestors directive, which controls who may embed your page in an iframe.
The catch is that a strict CSP will break things on a real site. Inline scripts, third-party analytics, embedded widgets, and injected styles all violate a naive policy, and a too-aggressive rule can take down your own functionality. This is why you never enforce a new CSP blind. Ship it in report-only mode first.
# Report-only: the browser reports violations but enforces nothing.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
# Once the reports are clean, switch the header name to enforce it.
Content-Security-Policy: default-src 'self'; script-src 'self'; frame-ancestors 'none'
With Content-Security-Policy-Report-Only, the browser sends you a report for every resource the policy would have blocked but loads the page normally. You watch the reports, widen the policy to cover the legitimate sources you forgot, and only rename the header to the enforcing Content-Security-Policy once the noise stops. The MDN reference on Content-Security-Policy documents every directive, and the OWASP Secure Headers Project has tested starting policies worth copying.
A reasonable first enforcing policy locks the default source to your own origin and forbids framing entirely:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'
HSTS and the headers that are safe to ship today
CSP needs care. Two headers do not, and you can roll them out broadly without breaking anything.
Strict-Transport-Security (HSTS) tells the browser to only ever connect to your site over HTTPS, for the duration you specify. After the first secure visit, the browser refuses plain HTTP entirely, which closes the gap a protocol-downgrade attack relies on: an attacker on the network can no longer transparently strip your connection back to HTTP. Because HSTS and HTTPS go together, it is worth confirming your certificate and HTTPS redirect are solid first with an SSL checker before you commit a browser to HTTPS-only for a year.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
The one caution: max-age is sticky. Start with a shorter window (a few hours) while you confirm every subdomain genuinely serves HTTPS, then raise it to a year. Add preload only once you are certain, since the HSTS preload list is slow to undo. See the MDN reference on Strict-Transport-Security for the details.
X-Content-Type-Options: nosniff is the other safe one. It stops the browser from second-guessing the Content-Type you declared and "sniffing" a different one from the file contents. That sniffing is how an uploaded file served as text/plain can get executed as JavaScript. The header has exactly one value, it has no downside, and you should send it on every response. The MDN reference on X-Content-Type-Options covers the one case it matters most.
X-Content-Type-Options: nosniff
The safe rollout order is simple: ship X-Content-Type-Options and HSTS broadly today, then build CSP carefully behind report-only mode.
Permissions-Policy, Referrer-Policy, and clickjacking
Three more headers round out a solid baseline.
Permissions-Policy locks down powerful browser features so a page (or an injected script, or an embedded third party) cannot quietly use them. If your site never needs the camera, microphone, or geolocation, denying them outright means a compromised dependency cannot turn them on either. The MDN reference on Permissions-Policy lists every controllable feature.
Permissions-Policy: camera=(), microphone=(), geolocation=()
For clickjacking, you have two overlapping tools. X-Frame-Options: DENY is the older header that stops your page from being framed; the modern equivalent is the CSP frame-ancestors directive shown above. Send both: frame-ancestors is more flexible and is what current browsers honor, while X-Frame-Options covers anything old enough to ignore CSP. See the MDN reference on X-Frame-Options.
X-Frame-Options: DENY
Referrer-Policy controls how much of the originating URL is sent when a visitor clicks a link off your site. A sensible default keeps the full path on same-origin navigation but trims it to just the origin when leaving, so you do not leak query strings or private path segments to third parties. The MDN reference on Referrer-Policy explains each value.
Referrer-Policy: strict-origin-when-cross-origin
Where to set them: Next.js, nginx, and the CDN
The headers are the same everywhere; only the place you declare them changes.
In Next.js, return them from the headers() async function in next.config.js. They apply to every route matched by source:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
],
},
];
},
};
On nginx, use add_header inside your server block. The always flag makes nginx send the header on error responses too, which you want:
# inside server { ... }
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
If you sit behind a CDN, you often do not need to touch your server at all. Cloudflare sets HSTS and custom response headers from the dashboard (under SSL/TLS and Rules), and Vercel reads the same headers config from next.config.js or a vercel.json file. Setting headers at the edge has a bonus: they apply to static assets and cached responses that never hit your origin. The tradeoff is one more place to check, since a header set on the origin and overridden at the edge is a common source of confusion.
Grading your coverage in one scan
Every header above is a one-line addition, but knowing which ones you are actually sending, on the live site, after the CDN and framework have had their say, means reading the real HTTP response. Doing that by hand with curl on every environment gets old fast, and it is easy to set a header in config that something downstream quietly strips.
The LintPage Security Headers Checker reads your live response headers and grades them in one request. It checks Content-Security-Policy against injection and clickjacking, Strict-Transport-Security for forced HTTPS, X-Content-Type-Options for MIME sniffing, Permissions-Policy for locked-down browser features, plus Referrer-Policy and X-Frame-Options, then rolls it all into an overall letter grade for your header coverage so you can see at a glance what is missing.
Headers rarely travel alone with their HTTPS setup, so it is worth running the SSL checker alongside this (HSTS only makes sense on a site whose certificate and redirects are already solid), and the full audit to catch the rest of the pre-launch issues in the same pass.
The 30-second version
Security headers are a few lines of server config that tell the browser how to defend your users, invisible to visitors and among the best return on effort in web security. Ship X-Content-Type-Options: nosniff and Strict-Transport-Security broadly today: they are safe and have no real downside. Build Content-Security-Policy carefully, because it is the most powerful header (mitigating XSS and, via frame-ancestors, clickjacking) and the easiest to break, so run it in Content-Security-Policy-Report-Only until the reports are clean before you enforce it. Add Permissions-Policy, Referrer-Policy, and X-Frame-Options for a complete baseline. Set them in the Next.js headers() function, with nginx add_header, or at the CDN, then scan the live response so you know what is actually going out. Google will not rank you higher for any of this, but a hacked site destroys rankings, and this is the cheapest way to not be one.