HTTP Security Headers: Essential Configuration Guide

Type: Software Reference Confidence: 0.95 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Essential Security Headers

#HeaderRecommended ValuePurposeRisk if Missing
1Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadForces HTTPS for all future connectionsMITM attacks, SSL stripping, cookie hijacking
2Content-Security-Policydefault-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'Controls allowed content sourcesXSS attacks, data injection, clickjacking
3X-Content-Type-OptionsnosniffPrevents MIME type sniffingMIME confusion attacks, script injection via uploaded files
4X-Frame-OptionsDENY or SAMEORIGINPrevents clickjacking via iframesClickjacking, UI redressing attacks
5Referrer-Policystrict-origin-when-cross-originControls referrer information leakageURL token leakage, privacy exposure
6Permissions-Policygeolocation=(), camera=(), microphone=()Restricts browser API accessThird-party scripts accessing sensors/camera/mic
7Cross-Origin-Opener-Policysame-originIsolates browsing contextSpectre-like side-channel attacks
8Cross-Origin-Resource-Policysame-siteControls cross-origin resource loadingData leakage via cross-origin reads
9Cross-Origin-Embedder-Policyrequire-corpRestricts cross-origin embeddingBlocks SharedArrayBuffer (needed for COOP isolation)
10X-XSS-Protection0Disables broken legacy XSS filterLegacy filter can introduce new XSS vectors

Headers to Remove (Information Disclosure)

#HeaderActionReason
1ServerRemove or set to generic valueReveals web server software and version
2X-Powered-ByRemove entirelyReveals framework (Express, PHP, ASP.NET)
3X-AspNet-VersionDisable in web.configReveals .NET version
4X-AspNetMvc-VersionDisable in Global.asaxReveals ASP.NET MVC version

Deprecated Headers (Do Not Use)

#HeaderStatusReplacement
1Expect-CTDeprecatedCertificate Transparency enforced by default
2Public-Key-Pins (HPKP)Removed from browsersUse Certificate Transparency instead
3Feature-PolicyReplacedUse Permissions-Policy
4X-XSS-Protection (with 1)DeprecatedUse CSP; set header to 0 to disable legacy filter

Decision Tree

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

Step-by-Step Guide

1. Audit existing security headers

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.

2. Set Strict-Transport-Security (HSTS)

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.

3. Deploy Content-Security-Policy

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.

4. Set remaining essential headers

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.

5. Remove information disclosure headers

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.

Code Examples

Express/Node.js: Helmet.js (recommended)

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' }
}));

Django: SecurityMiddleware Settings

# 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

Nginx: Security Headers Snippet

# /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;

Apache: VirtualHost Configuration

# 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

Caddy: Caddyfile Configuration

(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
}

Anti-Patterns

Wrong: Setting HSTS on HTTP responses

# BAD -- HSTS on port 80 is ignored by browsers and wastes bytes
server {
    listen 80;
    add_header Strict-Transport-Security "max-age=63072000" always;
}

Correct: HSTS only on HTTPS

# 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;
}

Wrong: CSP with unsafe-inline

# BAD -- unsafe-inline allows injected scripts to execute, defeating CSP
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

Correct: Nonce-based CSP

# GOOD -- only scripts with matching nonce execute
Content-Security-Policy: default-src 'self'; script-src 'nonce-abc123def456' 'strict-dynamic'

Wrong: X-XSS-Protection set to 1

# BAD -- the legacy XSS filter can be abused to selectively disable scripts
X-XSS-Protection: 1; mode=block

Correct: X-XSS-Protection disabled

# GOOD -- disable legacy filter; rely on CSP for XSS protection
X-XSS-Protection: 0

Wrong: Nginx headers in location blocks override server block headers

# 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
    }
}

Correct: Use include snippet for consistent headers

# 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";
    }
}

Wrong: HSTS preload without testing

# BAD -- submitting to preload list before verifying all subdomains use HTTPS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Correct: Gradual HSTS rollout

# 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

Common Pitfalls

Diagnostic Commands

# 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

Version History & Compatibility

Header/StandardStatusBrowser SupportKey Changes
Strict-Transport-SecurityRFC 6797 (2012)All modern browsersStable since 2012; preload lists maintained by all major browsers
Content-Security-Policy L3W3C CRChrome 59+, Firefox 58+, Safari 15.4+, Edge 79+strict-dynamic, report-to replaces report-uri
X-Content-Type-OptionsStandardAll modern browsersStable; nosniff is the only valid value
X-Frame-OptionsRFC 7034 (2013)All modern browsersBeing superseded by CSP frame-ancestors
Referrer-PolicyW3C CRAll modern browsersstrict-origin-when-cross-origin is default since 2021
Permissions-PolicyW3C DraftChrome 88+, Firefox 74+, Safari 16+, Edge 88+Replaced Feature-Policy; different syntax
Cross-Origin-Opener-PolicyStandardChrome 83+, Firefox 79+, Safari 15.2+, Edge 83+Required for SharedArrayBuffer
Cross-Origin-Resource-PolicyStandardChrome 73+, Firefox 74+, Safari 12+, Edge 79+Defends against Spectre-like attacks

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Any production website or APILocal development server (HSTS blocks HTTP)Skip HSTS in dev; use other headers
Site serves user-generated contentStatic site with zero dynamic contentMinimal CSP (default-src 'none') suffices
Multiple subdomains all on HTTPSSome subdomains HTTP-only or self-signedOmit includeSubDomains from HSTS
Embedding third-party contentSite is itself a public embeddable widgetX-Frame-Options: SAMEORIGIN or CSP frame-ancestors

Important Caveats

Related Units