Step-by-step guide to adding the 6 most important HTTP security headers. Works for Nginx, Apache, Cloudflare, Vercel, and Next.js.
Anay Pandya
1,477 words
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
HTTP security headers are instructions your server sends to browsers, telling them how to behave when handling your site's content. They are your first line of defense against common web attacks: cross-site scripting (XSS), clickjacking, MIME-type sniffing, and protocol downgrade attacks.
<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">Across 12,400+ sites in our scan corpus, 29% still accept TLS 1.1 connections — a protocol deprecated by RFC 8996 in March 2021 and flagged as insecure by every major browser.</p> </div>
Keep reading
Is your site actually secure?
Run a free check — 60 seconds
The problem? Most websites are missing some or all of them. A 2025 study by the HTTP Archive found that only 12% of websites serve a Content-Security-Policy header, and only 25% serve Strict-Transport-Security.
These headers cost nothing to implement, require no code changes, and can be deployed in minutes. There is no reason not to have them.
Here are the six security headers every website needs, in order of importance:
Forces HTTPS connections. Prevents protocol downgrade attacks.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadmax-age=31536000 — enforce for one yearincludeSubDomains — applies to all subdomainspreload — submit to browser preload listsControls which resources the browser is allowed to load. The single most effective header against XSS attacks.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';A strict CSP looks like this:
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';Start with report-only mode to avoid breaking your site:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;Prevents MIME-type sniffing. Browsers will only use the Content-Type you declare.
X-Content-Type-Options: nosniffOne value, one purpose. This is the easiest header to add.
Prevents your site from being embedded in iframes on other domains (clickjacking protection).
X-Frame-Options: DENYUse DENY to block all framing, or SAMEORIGIN to allow framing only by your own domain.
Note: frame-ancestors in CSP supersedes X-Frame-Options, but include both for backward compatibility with older browsers.
Controls how much referrer information is shared when users navigate away from your site.
Referrer-Policy: strict-origin-when-cross-originThis sends the full URL for same-origin requests and only the origin (domain) for cross-origin requests. It is a good balance between privacy and functionality.
Controls which browser features your site can use (camera, microphone, geolocation, etc.).
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()Empty parentheses () disable the feature entirely. If your site does not use the camera, declare it — this prevents malicious scripts from accessing it if they somehow execute on your page.
Add all six headers to your server block:
server {
listen 443 ssl http2;
server_name yoursite.com;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" 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=(), payment=()" always;
# SSL config
ssl_certificate /etc/ssl/certs/yoursite.crt;
ssl_certificate_key /etc/ssl/private/yoursite.key;
location / {
# ... your config
}
}Important Nginx caveat: If you use add_header inside a location block, it overrides all headers set at the server level. Either set all headers at the server level, or repeat them in every location block. Alternatively, use the ngx_headers_more module:
more_set_headers "X-Content-Type-Options: nosniff";Test and reload:
nginx -t && systemctl reload nginxEnable mod_headers and add the headers to your VirtualHost or .htaccess:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
</IfModule>Enable the module if it is not already active:
a2enmod headers
systemctl restart apache2Cloudflare offers multiple approaches:
*)export default {
async fetch(request) {
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
newResponse.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';");
newResponse.headers.set('X-Content-Type-Options', 'nosniff');
newResponse.headers.set('X-Frame-Options', 'DENY');
newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
newResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
return newResponse;
},
};Cloudflare has a dedicated HSTS toggle under SSL/TLS → Edge Certificates. Use it for HSTS and Transform Rules for the rest.
In next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';",
},
{
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=(), payment=()',
},
],
},
];
},
};
module.exports = nextConfig;Alternatively, use middleware for dynamic CSP with nonces:
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline';`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
return response;
}curl -sI https://yoursite.com | grep -iE '(strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy)'| Header | Expected value | Check |
|---|---|---|
| Strict-Transport-Security | max-age=31536000; ... | Present and max-age >= 31536000 |
| Content-Security-Policy | default-src 'self'; ... | Present and not empty |
| X-Content-Type-Options | nosniff | Exact match |
| X-Frame-Options | DENY or SAMEORIGIN | Present |
| Referrer-Policy | strict-origin-when-cross-origin | Present |
| Permissions-Policy | camera=(), ... | Present |
The fastest way to check all headers at once is to run a scan at zeriflow.com. The scanner checks all six headers plus additional security configurations, and gives you specific remediation steps for anything that is missing.
After adding these headers:
report-uri or report-to directives to catch issuesSecurity headers are the lowest-effort, highest-impact security improvement you can make to any website. There is no reason to leave them missing.
<!-- zf-internal-links -->
Check all your security headers instantly — free.
See exactly which headers are missing and how to fix them.
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
October 10, 2025
SME Security Starter Pack: 12 Controls That Actually Move the Needle
Monitor your security score
Schedule recurring scans and catch regressions after deployments.