Production-ready HTTP security headers for Apache, Nginx, Node.js, and more.
Security headers are your first line of defense against:
- XSS (Cross-Site Scripting) — malicious scripts injected into your site
- Clickjacking — invisible frames tricking users into clicking malicious content
- Data leaks — unauthorized access to cross-origin resources
- MIME-type attacks — browsers executing malicious content
- Referrer leaks — sensitive URL data exposed to third parties
- Base tag injection — attackers manipulating relative URLs
- FLoC/Topics tracking — privacy-invasive browser features
Result: Setting proper headers can boost your security rating from F to A+ in minutes, with zero code changes to your application.
curl -I https://yoursite.com | grep -i "x-frame\|content-security\|strict-transport"
# Or use online tools:
# https://securityheaders.com
# https://observatory.mozilla.orgAdd this to your .htaccess or Apache virtual host config:
# ============================================
# Security Headers — Production Config (2025)
# ============================================
# Hide PHP version (if using PHP)
php_flag expose_php off
<IfModule mod_headers.c>
# Isolation headers
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
# Content Security Policy
# OPTION 1: Strict (recommended for new sites)
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
# OPTION 2: Relaxed (for sites with inline scripts/styles)
# Header always set Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline'; connect-src 'self'; base-uri 'self'; object-src 'none'"
# Frame protection
Header always set X-Frame-Options "SAMEORIGIN"
# MIME-type protection
Header always set X-Content-Type-Options "nosniff"
# Download handling (Legacy IE8 - optional in 2025)
# Header always set X-Download-Options "noopen"
# Feature policies (2025 updated)
# Note: browsing-topics blocks Google's Topics API (FLoC successor)
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), browsing-topics=(), interest-cohort=()"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# HTTPS enforcement (only if you have SSL!)
# WARNING: Only enable if your ENTIRE site uses HTTPS permanently
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Adobe policy restrictions
Header always set X-Permitted-Cross-Domain-Policies "none"
</IfModule>
# ============================================
# Additional Security Measures
# ============================================
# Disable directory browsing
Options -Indexes
# Disable server signature
ServerSignature Off
# Block suspicious request methods
<LimitExcept GET POST HEAD>
deny from all
</LimitExcept>Header always vs Header set:
Header set→ Only applied to successful responses (2xx)Header always→ Applied to ALL responses including errors (3xx/4xx/5xx)- Always use
alwaysfor security headers to ensure protection even on error pages
# Enable mod_headers
sudo a2enmod headers
sudo systemctl restart apache2Add to your nginx.conf or site-specific config in /etc/nginx/sites-available/:
server {
listen 443 ssl http2;
server_name yoursite.com;
# Hide Nginx version
server_tokens off;
# Security headers
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# Content Security Policy (2025)
# Note: 'https:' allows loading from ANY https source - restrict to specific domains in production
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
# Frame protection
add_header X-Frame-Options "SAMEORIGIN" always;
# MIME-type protection
add_header X-Content-Type-Options "nosniff" always;
# Referrer policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# HTTPS enforcement
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Permissions policy (2025)
# browsing-topics blocks Google's Topics API (FLoC successor for ad targeting)
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), browsing-topics=(), interest-cohort=()" always;
# Adobe policies
add_header X-Permitted-Cross-Domain-Policies "none" always;
# Your site config continues here...
root /var/www/html;
index index.html;
}# Test configuration
sudo nginx -t
# Reload if successful
sudo systemctl reload nginxconst express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');
const app = express();
// Apply all security headers with 2025 best practices
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: "same-origin" },
crossOriginResourcePolicy: { policy: "same-origin" },
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
}));
// Additional Permissions-Policy header (Helmet doesn't cover all 2025 features yet)
app.use((req, res, next) => {
res.setHeader('Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=(), usb=(), browsing-topics=(), interest-cohort=()');
next();
});
app.get('/', (req, res) => {
res.send('Secured with Helmet!');
});
app.listen(3000);const crypto = require('crypto');
// Middleware to generate nonce per request
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
objectSrc: ["'none'"],
baseUri: ["'self'"],
},
},
}));
// Use nonce in your templates
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<script nonce="${res.locals.nonce}">
console.log('Inline script allowed with nonce!');
</script>
</head>
<body>Nonce-based CSP</body>
</html>
`);
});app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.setHeader('Content-Security-Policy', "default-src 'self'; object-src 'none'; base-uri 'self'");
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), browsing-topics=(), interest-cohort=()');
next();
});Create nginx-security.conf:
# Include this in your Nginx Docker image
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "interest-cohort=(), browsing-topics=()" always;Dockerfile:
FROM nginx:alpine
COPY nginx-security.conf /etc/nginx/conf.d/security.conf
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80 443Add headers via Workers or Transform Rules:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const response = await fetch(request)
// Clone response to modify headers
const newResponse = new Response(response.body, response)
// Add security headers (2025)
newResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin')
newResponse.headers.set('Cross-Origin-Resource-Policy', 'same-origin')
newResponse.headers.set('Content-Security-Policy', "default-src 'self'; object-src 'none'; base-uri 'self'")
newResponse.headers.set('X-Frame-Options', 'SAMEORIGIN')
newResponse.headers.set('X-Content-Type-Options', 'nosniff')
newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
newResponse.headers.set('Permissions-Policy', 'browsing-topics=(), interest-cohort=()')
return newResponse
}WordPress often requires relaxed CSP due to:
- Inline scripts in themes/plugins
- Third-party assets (fonts, analytics, CDNs)
- Admin panel dynamic content
<IfModule mod_headers.c>
# Isolation headers (safe for WordPress)
Header always set Cross-Origin-Opener-Policy "same-origin-allow-popups"
Header always set Cross-Origin-Resource-Policy "cross-origin"
# Relaxed CSP for WordPress (2025)
Header always set Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:; font-src 'self' https: data:; connect-src 'self' https:; base-uri 'self'; object-src 'none'"
# Frame protection (allows same-origin for admin)
Header always set X-Frame-Options "SAMEORIGIN"
# MIME protection
Header always set X-Content-Type-Options "nosniff"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), browsing-topics=(), interest-cohort=()"
# HSTS (only if SSL is configured)
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>If .htaccess doesn't work, use this in functions.php:
// Add security headers via PHP
function add_security_headers() {
header('Cross-Origin-Opener-Policy: same-origin-allow-popups');
header('Cross-Origin-Resource-Policy: cross-origin');
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=(), browsing-topics=(), interest-cohort=()');
header("Content-Security-Policy: default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; base-uri 'self'; object-src 'none'");
}
add_action('send_headers', 'add_security_headers');Purpose: Controls which resources can be loaded
Strict (2025 recommended):
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests
Key 2025 additions:
object-src 'none'— Blocks Flash and legacy pluginsbase-uri 'self'— Prevents base tag injection attacksupgrade-insecure-requests— Auto-upgrades HTTP to HTTPS
Relaxed (for legacy sites):
Content-Security-Policy: default-src 'self' https: data: 'unsafe-inline'; base-uri 'self'; object-src 'none'
Common CSP issues:
- Inline scripts blocked → Use nonces or move to external
.jsfiles - Google Analytics blocked → Add
script-src 'self' https://www.google-analytics.com - Fonts not loading → Add
font-src 'self' https://fonts.gstatic.com
CSP with Nonces (best practice):
<!-- Server generates nonce per request -->
<script nonce="2726c7f26c">
console.log('Allowed!');
</script>Subresource Integrity (SRI) for CDNs:
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous"></script>Purpose: Forces HTTPS for all connections
Configuration:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- Only enable if your entire site uses SSL permanently
- Once set, browsers will refuse HTTP for the specified duration
preloaddirective submits your domain to browser HSTS lists- Preload is IRREVERSIBLE — Removal takes months and requires support tickets
Preload Submission Process:
- Ensure HTTPS works on all subdomains
- Set
max-age=31536000minimum - Submit at https://hstspreload.org
- Wait for inclusion in Chromium/Firefox/Safari lists
- Cannot easily undo — Only submit if 100% certain
Safe rollout strategy:
# Week 1: Short max-age for testing
Header always set Strict-Transport-Security "max-age=300"
# Week 2-4: Increase gradually
Header always set Strict-Transport-Security "max-age=86400"
# Month 2+: Full deployment
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Only after 6+ months of stability:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"Purpose: Prevents clickjacking attacks
Options:
DENY— Never allow framingSAMEORIGIN— Allow framing from same domain only— (deprecated, use CSPALLOW-FROMframe-ancestorsinstead)
Modern alternative: Use CSP frame-ancestors directive:
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
Prevents browsers from MIME-sniffing responses away from declared content-type.
Why it matters: Without this, browsers might execute image.jpg as JavaScript if it contains code.
Options:
no-referrer— Never send referrer (breaks some analytics)strict-origin-when-cross-origin— (recommended) Full URL for same-origin, origin only for cross-originsame-origin— Only send for same-origin requests
Privacy consideration: Balance between analytics needs and user privacy.
2025/2026 updated syntax:
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=(), browsing-topics=(), interest-cohort=()
New in 2025+:
browsing-topics=()— Blocks Google Topics API (FLoC successor for ad targeting)interest-cohort=()— Blocks legacy FLoC (kept for older browser compatibility)
Common permissions:
geolocation— GPS locationmicrophone— Audio inputcamera— Video inputpayment— Payment Request APIusb— USB device accessmagnetometer,gyroscope— Motion sensors
Purpose: Isolates browsing context from cross-origin documents
Options:
same-origin— Strictest, breaks pop-upssame-origin-allow-popups— Allows pop-ups (better for WordPress)
Purpose: Prevents resources from being loaded by other origins
Options:
same-origin— Only same domainsame-site— Same site (includes subdomains)cross-origin— Allow all (least secure)
Purpose: Required for advanced features like SharedArrayBuffer
Configuration:
Cross-Origin-Embedder-Policy: require-corp
# WRONG — This header is deprecated and harmful
X-XSS-Protection: 1; mode=block
Why deprecated:
- Chrome removed support in 2019
- Safari implementation has security bugs
- Can introduce vulnerabilities instead of preventing them
- Superseded by Content-Security-Policy
Correct approach: Rely on CSP instead. If you must set it, disable it:
X-XSS-Protection: 0
-
Security Headers — https://securityheaders.com
Quick grade (A+ to F) with actionable recommendations -
Mozilla Observatory — https://observatory.mozilla.org
Comprehensive scan with detailed explanations -
CSP Evaluator — https://csp-evaluator.withgoogle.com
Validates Content-Security-Policy syntax -
HSTS Preload — https://hstspreload.org
Check preload eligibility and status
# Check all security headers
curl -I https://yoursite.com | grep -iE "content-security|x-frame|strict-transport|x-content|referrer"
# Test specific header
curl -I https://yoursite.com | grep -i "x-frame-options"
# Check from different location (for CDN testing)
curl -H "Host: yoursite.com" -I https://cdn-ip-address/
# Verify CSP is applied
curl -I https://yoursite.com | grep -i "content-security-policy"- Open DevTools (F12)
- Go to Network tab
- Refresh page
- Click on main document
- Check Response Headers section
- Look for CSP violations in Console tab
Setup CSP reporting:
# Apache
Header always set Content-Security-Policy "default-src 'self'; report-uri https://yoursite.com/csp-report"
# Or use report-only mode during testing
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri https://yoursite.com/csp-report"Node.js CSP report endpoint:
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
res.status(204).end();
});Symptom: Site appears broken, console shows CSP violations
Solution: Start with a relaxed policy, then tighten:
# Phase 1: Report-only mode (doesn't block, only logs)
Header set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report"
# Phase 2: After reviewing violations, enable blocking with relaxed policy
Header set Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline'; base-uri 'self'"
# Phase 3: Tighten after confirming no issues
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; base-uri 'self'; object-src 'none'"Symptom: Users can't access site even after fixing SSL
Solution:
- Renew SSL immediately
- Use short
max-ageinitially:max-age=300(5 minutes) - Gradually increase after confirming stability
- Never use
preloaduntil 100% certain
Symptom: Can't save posts, plugins fail to update
Solution: Use WordPress-specific config (see WordPress section)
Symptom: YouTube videos, Google Maps, etc. blocked
Solution: Adjust CSP frame-src and connect-src:
Header set Content-Security-Policy "default-src 'self'; frame-src 'self' https://www.youtube.com https://www.google.com; connect-src 'self' https://www.google-analytics.com"Solution: Add to CSP:
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com"Solution: Add font-src and style-src:
Header set Content-Security-Policy "default-src 'self'; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com"Begin with relaxed policies, monitor for issues, then tighten gradually.
Never deploy security headers directly to production without testing.
Implement CSP reporting to catch issues before users do.
Keep .htaccess / nginx.conf in Git to track changes.
If you must use 'unsafe-inline', document WHY in comments:
# 'unsafe-inline' required for:
# - WordPress admin dashboard (uses inline scripts)
# - Theme customizer (inline styles)
# TODO: Migrate to nonce-based CSP when possible
Header set Content-Security-Policy "default-src 'self' 'unsafe-inline'"Re-scan with securityheaders.com monthly to catch regressions.
Nonces allow specific inline scripts without blanket permission:
// Generate per request
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy', `script-src 'nonce-${nonce}'`);For all third-party scripts:
<script src="https://cdn.jsdelivr.net/npm/[email protected]"
integrity="sha384-..."
crossorigin="anonymous"></script><IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; base-uri 'self'; object-src 'none'"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "interest-cohort=(), browsing-topics=()"
</IfModule># Cloudflare already provides some headers, avoid duplication
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Permissions-Policy "interest-cohort=()" always;app.use(helmet({
contentSecurityPolicy: false, // Not needed for APIs
frameguard: { action: 'deny' },
hsts: { maxAge: 31536000 },
noSniff: true
}));Monitor network-level errors:
# Apache
Header always set NEL '{"report_to":"default","max_age":31536000,"include_subdomains":true}'
Header always set Report-To '{"group":"default","max_age":31536000,"endpoints":[{"url":"https://yoursite.com/nel-report"}],"include_subdomains":true}'// Node.js endpoint
app.post('/nel-report', express.json(), (req, res) => {
console.log('Network Error:', req.body);
res.status(204).end();
});Note: Deprecated as of June 2021 (CT now mandatory), but still useful for older browsers:
Header always set Expect-CT "max-age=86400, enforce, report-uri='https://yoursite.com/ct-report'"Improvements welcome!
- Fork repository
- Add your platform-specific config
- Test thoroughly
- Submit PR with documentation
MIT License — Use freely, modify as needed, no warranty provided.
Found this useful?
- ⭐ Star this repository
- 🐛 Report issues
- 💡 Suggest improvements
- 💖 Sponsor development
Stay secure. Stay paranoid. 🔒
- Security Headers — Complete Implementation Guide
- Securing FastAPI Applications
- ModSecurity Webserver Protection Guide
- GPT Security Best Practices
updated 11.12.2025