Web Security Headers: What They Do and How to Configure Them
A practical, copy-paste guide for Nginx, Apache, Cloudflare, Node/Express, Vercel, and Netlify

Small, surgical changes to your HTTP response headers can block whole classes of attacks and improve privacy for your users. This guide explains five essential headers — X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, Referrer-Policy, and Permissions-Policy — plus exact, copy-paste snippets for popular stacks.
Why Security Headers Matter
These headers work at the browser level to prevent clickjacking, MIME sniffing, protocol downgrades, data over-sharing, and unauthorized hardware/API access. They are low-risk to deploy (with a few caveats for HSTS) and provide immediate, measurable defenses.
Recommended Baseline (Safe Defaults)
- X-Frame-Options:
DENY
(orSAMEORIGIN
if your site must embed itself). - X-Content-Type-Options:
nosniff
. - Strict-Transport-Security:
max-age=31536000; includeSubDomains; preload
(HTTPS-only sites; start smaller during rollout). - Referrer-Policy:
strict-origin-when-cross-origin
(useno-referrer
for maximum privacy if analytics allow). - Permissions-Policy: disable unused features, e.g.
accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
.
Tip: If you already use a strong Content-Security-Policy
with frame-ancestors
, prefer that over X-Frame-Options (keep XFO for legacy browsers if desired).
X-Frame-Options (Anti-Clickjacking)
What it does: Prevents your pages from being embedded in iframes on malicious origins that might trick users into clicking hidden UI (clickjacking).
Recommended values: DENY
blocks all framing. SAMEORIGIN
allows iframes from your own origin (useful for admin dashboards or widgets).
Risk without it: Attackers can overlay your site in an invisible frame and capture clicks or keystrokes.
How to add:
Nginx
add_header X-Frame-Options "SAMEORIGIN" always;
Apache (.htaccess or vhost)
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>
Cloudflare (Response Header Modification)
- Rules → Transform Rules → Response Header Modification → Add
- Header:
X-Frame-Options
, Value:SAMEORIGIN
Node/Express (helmet)
// npm i helmet
import helmet from "helmet";
app.use(helmet({ frameguard: { action: "sameorigin" } }));
Vercel (vercel.json)
{
"headers": [
{ "source": "/(.*)", "headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" }
] }
]
}
Netlify (netlify.toml)
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "SAMEORIGIN"
Notes: If you must allow specific external embedders, use CSP’s frame-ancestors
instead of loosening XFO across the board.
X-Content-Type-Options (No MIME Sniffing)
What it does: Stops browsers from “sniffing” content types and interpreting files as something else (e.g., serving user-uploaded text as executable script).
Recommended value: nosniff
.
Risk without it: Uploaded or proxied files may execute unexpectedly in some browsers.
How to add:
Nginx
add_header X-Content-Type-Options "nosniff" always;
Apache
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
</IfModule>
Cloudflare
- Add header:
X-Content-Type-Options
→nosniff
Node/Express (helmet)
import helmet from "helmet";
app.use(helmet({ contentTypeOptions: true })); // sends X-Content-Type-Options: nosniff
Vercel / Netlify
// Vercel header
{ "key": "X-Content-Type-Options", "value": "nosniff" }
// Netlify
X-Content-Type-Options = "nosniff"
Strict-Transport-Security (HSTS, HTTPS-Only)
What it does: Forces browsers to use HTTPS for your domain (and optionally subdomains), preventing protocol downgrades and certain MITM attacks.
Recommended value: max-age=31536000; includeSubDomains; preload
for mature, HTTPS-only estates. During initial rollout, start safer with max-age=86400
and without preload
.
Risk without it: Attackers can attempt SSL stripping or downgrade users to HTTP.
Pre-deployment checklist: Confirm all subdomains serve valid HTTPS; confirm redirects and CDNs won’t break. Understand that preload
submits your domain to browser lists and is hard to roll back.
How to add:
Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Apache
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>
Cloudflare
- SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS) → Enable and configure.
Node/Express (helmet)
import helmet from "helmet";
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));
Vercel / Netlify
// Vercel header
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains; preload" }
// Netlify
Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
Notes: Only set HSTS on HTTPS responses (ideally at the CDN/edge). Never add HSTS to HTTP because clients won’t see it in time.
Referrer-Policy (Privacy & Analytics Balance)
What it does: Controls how much referrer information the browser sends to other sites when users follow links.
Recommended value: strict-origin-when-cross-origin
— preserves full path on same-origin, but only the origin for cross-origin requests (good privacy/analytics balance). For maximum privacy use no-referrer
.
Risk without it: Unintended leakage of sensitive path/query data to third-party sites.
How to add:
Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Apache
<IfModule mod_headers.c>
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Cloudflare
- Add header:
Referrer-Policy
→strict-origin-when-cross-origin
Node/Express (helmet)
import helmet from "helmet";
app.use(helmet({ referrerPolicy: { policy: "strict-origin-when-cross-origin" } }));
Vercel / Netlify
// Vercel header
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
// Netlify
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy (Limit Powerful Browser Features)
What it does: Controls access to sensitive APIs and hardware (camera, mic, geolocation, autoplay, etc.). It prevents unused features from being abused by third-party widgets or compromised scripts.
Recommended baseline: Deny everything you do not explicitly need:
accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
Risk without it: Third-party content may prompt or access capabilities you didn’t intend to expose.
How to add:
Nginx
add_header Permissions-Policy "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always;
Apache
<IfModule mod_headers.c>
Header always set Permissions-Policy "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
</IfModule>
Cloudflare
- Add header:
Permissions-Policy
→ value as above.
Node/Express
// helmet v7 may not include a helper for Permissions-Policy; set it manually:
app.use((_, res, next) => {
res.setHeader("Permissions-Policy", "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
next();
});
Vercel / Netlify
// Vercel header
{ "key": "Permissions-Policy", "value": "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" }
// Netlify
Permissions-Policy = "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
Notes: If you need to allow a capability, scope it narrowly (e.g., camera=(self "https://video.example")
).
All-in-One Snippets (Copy/Paste)
Nginx (inside HTTPS server block)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always;
Apache (vhost or .htaccess; requires mod_headers)
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
</IfModule>
Cloudflare (Edge)
- Rules → Response Header Modification → Add each header/value above.
- HSTS: SSL/TLS → Edge Certificates → HTTP Strict Transport Security.
Node/Express
// npm i helmet
import helmet from "helmet";
app.use(helmet({
frameguard: { action: "sameorigin" }, // or 'deny'
contentTypeOptions: true, // nosniff
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
}));
// Permissions-Policy manual header:
app.use((_, res, next) => {
res.setHeader("Permissions-Policy", "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
next();
});
Vercel (vercel.json)
{
"headers": [
{ "source": "/(.*)", "headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains; preload" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" }
] }
]
}
Netlify (netlify.toml)
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "SAMEORIGIN"
X-Content-Type-Options = "nosniff"
Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
Deployment Order, Rollout, and Caveats
- Start with X-Content-Type-Options and Referrer-Policy (low risk).
- Add X-Frame-Options (verify iframes still work; consider CSP
frame-ancestors
for granular allow-lists). - Roll out Permissions-Policy (deny all, then open specific features as needed).
- Deploy HSTS last: begin with
max-age=86400
, verify logs and redirects, then increase to a year and considerpreload
.
Common pitfalls: mixed content, header duplication at CDN and origin, caching stale headers, enabling HSTS when some subdomains still serve HTTP, or breaking embedded integrations with too-strict framing rules.
Testing and Verification
- Command line:
curl -I https://yourdomain.com
and check headers. - Automated scanners: use multiple tools (observatory, securityheaders analyzers) to catch misconfigurations.
- Real-user testing: validate journeys involving iframes, payments, SSO, and third-party widgets.
- Monitoring: watch error rates, CSP reports (if using CSP), and browser console warnings.
Quick Mapping: Threat → Header
- Clickjacking → X-Frame-Options (or CSP
frame-ancestors
) - MIME sniffing → X-Content-Type-Options
- Protocol downgrade / SSL stripping → HSTS
- Leaky referral paths → Referrer-Policy
- Unwanted hardware/API access → Permissions-Policy
Summary
Implementing these five headers with the recommended values provides strong, immediate protection with minimal work. Use the all-in-one snippets above, ship in stages, and monitor. For most sites, this baseline is a “must-have” — simple, robust, and future-proof.
If you want to quickly check your website or any other website for security settings, install the SalesPilot extension, which will show you all the areas that require your attention.