How to Fix Missing Security Headers: Step-by-Step for Every Platform
You scanned your site and the report came back with a bad grade. The biggest culprit? Missing security headers. This is the most common issue we see — and fortunately, the fastest to fix. In this step-by-step guide, we'll go from failing headers to a clean bill of health on every major platform.
First, Know What You're Missing
Run a scan at WebSentry to get your baseline. The report will show each header as pass, warn, or fail. Focus on the fails first — those are the headers that are completely absent.
The most commonly missing headers, in order of frequency:
- Content-Security-Policy — missing on ~90% of sites
- Permissions-Policy — missing on ~85% of sites
- Referrer-Policy — missing on ~70% of sites
- X-Frame-Options — missing on ~50% of sites
- X-Content-Type-Options — missing on ~45% of sites
Fix 1: Cloudflare Pages / Workers Sites
If your site is on Cloudflare Pages, the simplest approach is a _headers file in your build output directory:
# _headers file (place in /public or build root)
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https:;Deploy again and rescan. Most headers will flip from fail to pass immediately.
Fix 2: Nginx
Add these directives to your server block. The always keyword ensures headers are set even on error responses (which are often forgotten):
server {
listen 443 ssl http2;
server_name yoursite.com;
# Security headers
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;
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' https:;" always;
# Remove server info
server_tokens off;
# ... rest of your config
}Run nginx -t to test the config, then sudo systemctl reload nginx.
Fix 3: Apache / .htaccess
On Apache, add headers to your .htaccess or virtual host config. Make sure mod_headers is enabled:
# .htaccess
<IfModule mod_headers.c>
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=()"
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' https:;"
# Remove version info
Header always unset X-Powered-By
Header always unset Server
ServerTokens Prod
</IfModule>Fix 4: Express.js / Node.js
The helmet middleware handles everything in one line:
npm install helmetconst express = require('express');
const helmet = require('helmet');
const app = express();
// Sets 11+ security headers with sensible defaults
app.use(helmet());
// Customize specific headers
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
}));
// Remove Express fingerprint
app.disable('x-powered-by');Fix 5: Next.js
Set headers in next.config.js:
// next.config.js
const securityHeaders = [
{ 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=()' },
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};Fix 6: Vercel
Add headers to your vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "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=()" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains; preload" }
]
}
]
}Fix 7: Django
Django has built-in security middleware. Enable it in settings.py:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# ... other middleware
]
# Security headers
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# For CSP, use django-csp package
# pip install django-csp
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'",)The Tricky One: Content-Security-Policy
CSP is the header most likely to break things. A restrictive policy will block third-party scripts (analytics, chat widgets, payment forms) unless you explicitly allowlist their domains. Here's a practical approach:
- Start with report-only: Use
Content-Security-Policy-Report-Onlyinstead ofContent-Security-Policy. This logs violations without blocking anything. - Monitor violations for a week: Check your browser console for CSP violations. Each reported violation tells you exactly which resource needs allowlisting.
- Build your allowlist: Add legitimate domains to the appropriate directives. For example, if you use Google Analytics, add
script-src 'self' https://www.googletagmanager.com. - Switch to enforcing: Once violations are zero, change the header from report-only to enforcing.
Verify Your Fix
After deploying header changes, don't assume they're working. CDNs, reverse proxies, and load balancers can strip or override headers. Always rescan after deployment.
With WebSentry, you can rescan instantly to verify each header is now passing. Watch your grade jump from C or D range up to A — it's one of the most satisfying improvements you can make in web development.
Keep Them From Regressing
Headers are set once but can disappear during infrastructure changes. Protect against regression:
- Add a CI check: Include a security header check in your deployment pipeline. Fail the build if critical headers are missing.
- Set up monitoring: WebSentry's Pro plan includes automated monitoring that re-scans your site on a schedule and alerts you if the grade drops.
- Document your headers: Keep a record of which headers you set and why, so the next developer who touches the config knows not to remove them.
Bottom Line
Missing security headers are the lowest-hanging fruit in web security. They take 10 minutes to add, cost nothing, and protect your users from XSS, clickjacking, data sniffing, and protocol downgrade attacks. Your scan report tells you exactly which ones to add — run a free scan and fix them today.