How to Fix Missing Security Headers (Nginx, Apache, Cloudflare, Express, Caddy)
You ran a security scan and found missing headers. Good — now let's fix them. This guide gives you copy-paste configs for every critical security header, for every major server.
Security headers are HTTP response headers that tell browsers how to behave when handling your site's content. Without them, browsers use permissive defaults that leave your users vulnerable to XSS, clickjacking, MIME sniffing, and data leakage.
sequenceDiagram
participant B as Browser
participant S as Server
B->>S: GET /page
S->>B: 200 OK + Security Headers
Note right of B: Browser enforces:<br/>HSTS → force HTTPS<br/>CSP → block bad scripts<br/>X-Frame → block iframes<br/>Referrer → limit URL leaking
If you want to understand the theory behind each header in depth, read our Security Headers Explained guide. This post is about fixing — getting every header configured correctly on your server, right now.
1. Strict-Transport-Security (HSTS)
What it does: Tells browsers to always use HTTPS for your domain. After the first visit, the browser will refuse to connect over HTTP, even if the user types http://.
Without it: An attacker on the same network (coffee shop Wi-Fi, hotel, airport) can intercept the initial HTTP request before the redirect to HTTPS and inject malicious content. This is a real, practical attack — not theoretical.
HSTS is also the header that upgrades your SSL grade from A to A+ in most scanners. See our SSL rating guide for more on that.
Recommended value: max-age=31536000; includeSubDomains; preload
This tells browsers to enforce HTTPS for one year, including all subdomains, and signals eligibility for the HSTS preload list.
Nginx
# In your server block
# Note: add_header directives in a location block override those from parent blocks
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Apache
# In .htaccess or <VirtualHost> (requires mod_headers)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Cloudflare
Cloudflare has built-in HSTS support:
SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS)
Enable it, set max-age to 12 months, enable includeSubDomains and preload.
Express
// Using helmet (recommended)
const helmet = require('helmet');
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));
Caddy
# In your Caddyfile
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Verify: curl -I https://yourdomain.com | grep -i strict
2. Content-Security-Policy (CSP)
What it does: Controls which resources (scripts, styles, images, fonts, frames) the browser is allowed to load on your page. It is the single most effective defense against XSS attacks.
Without it: A single XSS vulnerability lets an attacker inject any script from any source. With CSP, even if they find an injection point, the browser blocks scripts that aren't on your allowlist.
WARNING: Do NOT copy a CSP policy and deploy it to production without testing. A restrictive policy will break your site if it loads resources from CDNs, analytics providers, font services, or any external source. Start with report-only mode.
Start with Report-Only Mode
Report-only mode logs violations without blocking anything. Deploy this first, check the console for violations, then adjust your policy before enforcing.
Nginx
# Start with report-only to test (won't break anything)
add_header Content-Security-Policy-Report-Only "default-src 'self'" always;
# Once verified, switch to enforcing:
# add_header Content-Security-Policy "default-src 'self'" always;
Apache
# Start with report-only
Header always set Content-Security-Policy-Report-Only "default-src 'self'"
# Once verified, switch to enforcing:
# Header always set Content-Security-Policy "default-src 'self'"
Cloudflare
Cloudflare Dashboard → Rules → Transform Rules → Modify Response Header
Set header: Content-Security-Policy-Report-Only
Value: default-src 'self'
Express
// Using helmet (recommended)
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: { defaultSrc: ["'self'"] },
reportOnly: true // Remove this line once verified
}));
Caddy
# Start with report-only
header Content-Security-Policy-Report-Only "default-src 'self'"
Verify: curl -I https://yourdomain.com | grep -i content-security
CSP is a deep topic. default-src 'self' is a starting point. You will likely need to add specific sources for scripts, styles, images, and fonts that your site uses. Check browser console for violation reports and adjust accordingly.
3. X-Content-Type-Options
What it does: Prevents browsers from guessing ("sniffing") the MIME type of a response. The browser uses exactly what the server declares.
Without it: An attacker could upload a file with a .txt extension that contains JavaScript. The browser might sniff the content, decide it is actually JavaScript, and execute it.
Recommended value: nosniff (the only valid value)
Nginx
add_header X-Content-Type-Options "nosniff" always;
Apache
Header always set X-Content-Type-Options "nosniff"
Cloudflare
Cloudflare Dashboard → Rules → Transform Rules → Modify Response Header
Set header: X-Content-Type-Options
Value: nosniff
Express
const helmet = require('helmet');
app.use(helmet.noSniff());
Caddy
header X-Content-Type-Options "nosniff"
Verify: curl -I https://yourdomain.com | grep -i x-content-type
4. X-Frame-Options
What it does: Controls whether your site can be embedded in an iframe. Prevents clickjacking attacks where an attacker overlays invisible iframes over legitimate UI to hijack clicks.
Without it: An attacker creates a page with your site in a hidden iframe, positions a "Click here to win" button over your "Transfer funds" button, and the victim clicks the wrong thing.
Recommended value: DENY (blocks all framing) or SAMEORIGIN (allows framing only from your own domain)
Note: The modern replacement is CSP frame-ancestors directive. If you set Content-Security-Policy: frame-ancestors 'none', you don't need X-Frame-Options. But X-Frame-Options is still widely used for backward compatibility with older browsers.
Nginx
add_header X-Frame-Options "DENY" always;
Apache
Header always set X-Frame-Options "DENY"
Cloudflare
Cloudflare Dashboard → Rules → Transform Rules → Modify Response Header
Set header: X-Frame-Options
Value: DENY
Express
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));
Caddy
header X-Frame-Options "DENY"
Verify: curl -I https://yourdomain.com | grep -i x-frame
5. Referrer-Policy
What it does: Controls how much URL information the browser sends in the Referer header when navigating to another site.
Without it: When a user clicks a link on your site, the full URL — including query parameters — is sent to the destination. If your URLs contain session tokens, search queries, or private paths, those leak to third parties.
Recommended value: strict-origin-when-cross-origin
This sends the full URL for same-origin requests (your site to your site) but only the origin (e.g., https://yourdomain.com) for cross-origin requests. It is the best balance between functionality and privacy.
Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Apache
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Cloudflare
Cloudflare Dashboard → Rules → Transform Rules → Modify Response Header
Set header: Referrer-Policy
Value: strict-origin-when-cross-origin
Express
const helmet = require('helmet');
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));
Caddy
header Referrer-Policy "strict-origin-when-cross-origin"
Verify: curl -I https://yourdomain.com | grep -i referrer
6. Permissions-Policy
What it does: Restricts which browser APIs your site and any embedded third-party content can access — camera, microphone, geolocation, payment, USB, and more.
Without it: A third-party script (analytics, ads, chat widget) embedded on your page could silently request access to the user's camera or location.
Recommended value: camera=(), microphone=(), geolocation=()
The () syntax means "deny to everyone, including the page itself." Add specific origins if your site actually needs these features.
Nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Apache
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Cloudflare
Cloudflare Dashboard → Rules → Transform Rules → Modify Response Header
Set header: Permissions-Policy
Value: camera=(), microphone=(), geolocation=()
Express
// Manual — helmet doesn't have a dedicated Permissions-Policy method
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
Caddy
header Permissions-Policy "camera=(), microphone=(), geolocation=()"
Verify: curl -I https://yourdomain.com | grep -i permissions
Bonus: Other Headers
These headers are less common and serve more specific purposes. Most sites don't need them.
Cross-Origin-Opener-Policy (COOP): Isolates your browsing context from cross-origin windows. Required if you need SharedArrayBuffer (e.g., for WebAssembly threads). Set to same-origin if needed.
Cross-Origin-Resource-Policy (CORP): Prevents other sites from loading your resources (images, scripts, fonts) via cross-origin requests. Useful for API endpoints that should only be consumed by your own frontend. Set to same-origin or same-site.
Cross-Origin-Embedder-Policy (COEP): Works with COOP to enable full cross-origin isolation. Set to require-corp if needed — but be aware this requires all loaded resources to explicitly allow cross-origin loading.
X-XSS-Protection: This header is deprecated. Modern browsers (Chrome, Firefox, Edge) have removed the built-in XSS auditor it controlled. The auditor itself could be exploited in some cases. Set to 0 to explicitly disable it, or simply don't set it at all. If you have CSP configured, this header is entirely unnecessary.
All Headers at Once
Want to just copy one block and be done? Here are all six critical headers together. Remember: the CSP is in report-only mode — test it before switching to enforcing.
Nginx
# Security headers — add to your server block
# Note: add_header directives in a location block override those from parent blocks
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'" 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;
Apache
# Security headers — add to .htaccess or <VirtualHost> (requires mod_headers)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy-Report-Only "default-src 'self'"
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=()"
Caddy
# Security headers — add to your Caddyfile site block
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Content-Security-Policy-Report-Only "default-src 'self'"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
Express
// Security headers with helmet
const helmet = require('helmet');
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));
app.use(helmet.contentSecurityPolicy({
directives: { defaultSrc: ["'self'"] },
reportOnly: true // Remove once verified
}));
app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
Verify Your Fix
After deploying your changes, verify the headers are present:
curl -I https://yourdomain.com
Or scan your domain at siteprobe.live for a full security audit — we check all 10 headers, grade them, and show you exactly what's missing with server-specific fix snippets for your detected server type.
For deeper background on what each header does and how browsers enforce them, read our Security Headers Explained guide.
Check your domain now
See how your domain scores on SSL, security headers, and more.
Then set up monitoring alerts to catch problems early.