curl -sI https://your-site.com | grep -iE "strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy"includeSubDomains when not all subdomains support HTTPS -- this locks out legitimate traffic with no easy undo.includeSubDomains requires ALL subdomains to support HTTPS -- misconfiguration blocks legitimate trafficpreload is irreversible in practice -- once submitted to browser preload lists, removal takes monthsunsafe-inline defeats XSS protection -- use nonce-based or hash-based policies insteadX-XSS-Protection MUST be set to 0 -- the filter is deprecated and can introduce vulnerabilities in older browsersadd_header directives in inner blocks override ALL headers from outer blocks -- use include snippets| # | Header | Recommended Value | Purpose | Risk if Missing |
|---|---|---|---|---|
| 1 | Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | Forces HTTPS for all future connections | MITM attacks, SSL stripping, cookie hijacking |
| 2 | Content-Security-Policy | default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none' | Controls allowed content sources | XSS attacks, data injection, clickjacking |
| 3 | X-Content-Type-Options | nosniff | Prevents MIME type sniffing | MIME confusion attacks, script injection via uploaded files |
| 4 | X-Frame-Options | DENY or SAMEORIGIN | Prevents clickjacking via iframes | Clickjacking, UI redressing attacks |
| 5 | Referrer-Policy | strict-origin-when-cross-origin | Controls referrer information leakage | URL token leakage, privacy exposure |
| 6 | Permissions-Policy | geolocation=(), camera=(), microphone=() | Restricts browser API access | Third-party scripts accessing sensors/camera/mic |
| 7 | Cross-Origin-Opener-Policy | same-origin | Isolates browsing context | Spectre-like side-channel attacks |
| 8 | Cross-Origin-Resource-Policy | same-site | Controls cross-origin resource loading | Data leakage via cross-origin reads |
| 9 | Cross-Origin-Embedder-Policy | require-corp | Restricts cross-origin embedding | Blocks SharedArrayBuffer (needed for COOP isolation) |
| 10 | X-XSS-Protection | 0 | Disables broken legacy XSS filter | Legacy filter can introduce new XSS vectors |
| # | Header | Action | Reason |
|---|---|---|---|
| 1 | Server | Remove or set to generic value | Reveals web server software and version |
| 2 | X-Powered-By | Remove entirely | Reveals framework (Express, PHP, ASP.NET) |
| 3 | X-AspNet-Version | Disable in web.config | Reveals .NET version |
| 4 | X-AspNetMvc-Version | Disable in Global.asax | Reveals ASP.NET MVC version |
| # | Header | Status | Replacement |
|---|---|---|---|
| 1 | Expect-CT | Deprecated | Certificate Transparency enforced by default |
| 2 | Public-Key-Pins (HPKP) | Removed from browsers | Use Certificate Transparency instead |
| 3 | Feature-Policy | Replaced | Use Permissions-Policy |
| 4 | X-XSS-Protection (with 1) | Deprecated | Use CSP; set header to 0 to disable legacy filter |
START: What web server or framework are you using?
├── Nginx?
│ ├── YES → Use add_header in https server block; create /etc/nginx/snippets/security-headers.conf
│ └── NO ↓
├── Apache?
│ ├── YES → Enable mod_headers; use Header always set in VirtualHost or .htaccess
│ └── NO ↓
├── Caddy?
│ ├── YES → Use header directive in Caddyfile; HTTPS is automatic, add HSTS manually
│ └── NO ↓
├── Express/Node.js?
│ ├── YES → npm install helmet; app.use(helmet()) sets 11 headers with sane defaults
│ └── NO ↓
├── Django?
│ ├── YES → Configure SecurityMiddleware: SECURE_HSTS_SECONDS, SECURE_CONTENT_TYPE_NOSNIFF, etc.
│ └── NO ↓
├── CDN/Edge (Cloudflare, AWS)?
│ ├── YES → Configure via edge rules or transform rules
│ └── NO ↓
└── DEFAULT → Set headers at the reverse proxy level for consistent coverage
Before adding headers, check what your site currently sends. Use curl or an online scanner. [src4]
# Check all security headers at once
curl -sI https://your-site.com | grep -iE \
"strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy"
Verify: You should see each header listed. Missing headers indicate gaps to address.
Start with a short max-age and increase after confirming HTTPS works on all subdomains. [src2]
# Start conservative (5 minutes), ramp up to 2 years
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Verify: curl -sI https://your-site.com | grep -i strict-transport should show the header.
Start in report-only mode to avoid breaking your site. Progressively tighten the policy. [src1]
# Phase 1: Report-only (no enforcement)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
# Phase 2: Enforce
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'
Verify: Open DevTools > Console. CSP violations appear as errors.
These headers are straightforward and rarely cause breakage. Apply all at once. [src3]
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
X-XSS-Protection: 0
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(), usb=()
Verify: curl -sI https://your-site.com should show all headers. Score A+ on securityheaders.com.
Strip server identification headers to reduce fingerprinting surface. [src1]
# Nginx: hide server version
server_tokens off;
Verify: curl -sI https://your-site.com | grep -iE "server:|x-powered-by" should show no version info.
const express = require('express'); // ^4.18.0
const helmet = require('helmet'); // ^8.0.0
const app = express();
// Default: sets 11 headers with secure defaults
app.use(helmet());
// Custom: override specific headers
app.use(helmet({
strictTransportSecurity: {
maxAge: 63072000,
includeSubDomains: true,
preload: true
},
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
frameAncestors: ["'none'"]
}
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
# settings.py -- Django SecurityMiddleware handles most headers
SECURE_HSTS_SECONDS = 63072000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
SECURE_SSL_REDIRECT = True
# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src '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 X-XSS-Protection "0" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
server_tokens off;
# Enable mod_headers: a2enmod headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src '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 X-XSS-Protection "0"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always unset X-Powered-By
ServerTokens Prod
(security-headers) {
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
X-XSS-Protection "0"
Permissions-Policy "geolocation=(), camera=(), microphone=()"
-Server
}
}
example.com {
import security-headers
root * /var/www/html
file_server
}
# BAD -- HSTS on port 80 is ignored by browsers and wastes bytes
server {
listen 80;
add_header Strict-Transport-Security "max-age=63072000" always;
}
# GOOD -- HSTS on HTTPS only; HTTP redirects to HTTPS
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
}
# BAD -- unsafe-inline allows injected scripts to execute, defeating CSP
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
# GOOD -- only scripts with matching nonce execute
Content-Security-Policy: default-src 'self'; script-src 'nonce-abc123def456' 'strict-dynamic'
# BAD -- the legacy XSS filter can be abused to selectively disable scripts
X-XSS-Protection: 1; mode=block
# GOOD -- disable legacy filter; rely on CSP for XSS protection
X-XSS-Protection: 0
# BAD -- adding a header in location silently drops ALL server-level headers
server {
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
location /api {
add_header Cache-Control "no-store";
# X-Frame-Options and X-Content-Type-Options are now GONE for /api
}
}
# GOOD -- snippet ensures all headers present in every context
server {
include snippets/security-headers.conf;
location /api {
include snippets/security-headers.conf;
add_header Cache-Control "no-store";
}
}
# BAD -- submitting to preload list before verifying all subdomains use HTTPS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# GOOD -- start small, verify, then increase
# Week 1: Strict-Transport-Security: max-age=300
# Week 2: Strict-Transport-Security: max-age=86400; includeSubDomains
# Month 2: Strict-Transport-Security: max-age=63072000; includeSubDomains
# Month 3+: Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
includeSubDomains when dev/staging subdomains don't have HTTPS makes them unreachable. Fix: Audit all subdomains for HTTPS support before enabling includeSubDomains. [src2]add_header in a location block drops all headers from parent server block. Fix: Use an include snippet in every block that adds headers. [src7]Content-Security-Policy-Report-Only and review violations. [src1]Feature-Policy used geolocation 'none' but Permissions-Policy uses geolocation=(). Mixing formats silently fails. Fix: Use only Permissions-Policy with new syntax. [src5]always in Nginx: Without always, headers only appear on 2xx/3xx responses. Error pages lack security headers. Fix: Always use add_header ... always;. [src7]X-Frame-Options only supports DENY/SAMEORIGIN. For granular control, use CSP frame-ancestors. Both can coexist. Fix: Use CSP frame-ancestors for new deployments; keep X-Frame-Options for legacy browsers. [src1]# Audit all security headers
curl -sI https://your-site.com | grep -iE \
"strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy|x-xss|cross-origin|x-powered-by|server:"
# Check HSTS preload eligibility: https://hstspreload.org
# Mozilla Observatory: https://developer.mozilla.org/en-US/observatory
# securityheaders.com: https://securityheaders.com/?q=your-site.com
# CSP Evaluator: https://csp-evaluator.withgoogle.com/
# Check HSTS cache in Chrome: chrome://net-internals/#hsts
# Verify Nginx config before reload
nginx -t && nginx -s reload
# Check Apache modules (need mod_headers)
apachectl -M | grep headers
| Header/Standard | Status | Browser Support | Key Changes |
|---|---|---|---|
| Strict-Transport-Security | RFC 6797 (2012) | All modern browsers | Stable since 2012; preload lists maintained by all major browsers |
| Content-Security-Policy L3 | W3C CR | Chrome 59+, Firefox 58+, Safari 15.4+, Edge 79+ | strict-dynamic, report-to replaces report-uri |
| X-Content-Type-Options | Standard | All modern browsers | Stable; nosniff is the only valid value |
| X-Frame-Options | RFC 7034 (2013) | All modern browsers | Being superseded by CSP frame-ancestors |
| Referrer-Policy | W3C CR | All modern browsers | strict-origin-when-cross-origin is default since 2021 |
| Permissions-Policy | W3C Draft | Chrome 88+, Firefox 74+, Safari 16+, Edge 88+ | Replaced Feature-Policy; different syntax |
| Cross-Origin-Opener-Policy | Standard | Chrome 83+, Firefox 79+, Safari 15.2+, Edge 83+ | Required for SharedArrayBuffer |
| Cross-Origin-Resource-Policy | Standard | Chrome 73+, Firefox 74+, Safari 12+, Edge 79+ | Defends against Spectre-like attacks |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Any production website or API | Local development server (HSTS blocks HTTP) | Skip HSTS in dev; use other headers |
| Site serves user-generated content | Static site with zero dynamic content | Minimal CSP (default-src 'none') suffices |
| Multiple subdomains all on HTTPS | Some subdomains HTTP-only or self-signed | Omit includeSubDomains from HSTS |
| Embedding third-party content | Site is itself a public embeddable widget | X-Frame-Options: SAMEORIGIN or CSP frame-ancestors |
preload submits your domain to a hardcoded list in all major browsers -- if you later need HTTP, you cannot easily undo thisreport-uri is deprecated in favor of report-to (Reporting API), but report-to is not yet supported in all browsers -- use both during transitionPermissions-Policy is a W3C Draft and available directives change between browser versions -- check MDN for current supportCross-Origin-Embedder-Policy: require-corp breaks cross-origin resources without Cross-Origin-Resource-Policy -- audit third-party resources firstServer header cannot always be fully removed -- some web servers add it after application-level headers