Web Security Headers Fundamentals

HTTP response headers are the primary mechanism for instructing browsers on how to enforce transport security, restrict resource execution, and limit information leakage. For developers, sysadmins, and security engineers, a coherent header strategy is a non-negotiable baseline for modern application defense. This reference establishes production-ready configurations aligned with current browser security models, with explicit security trade-offs for each deployment decision. The detailed mechanics of individual controls live in the dedicated references for Content Security Policy, HTTP Strict Transport Security, and cross-origin isolation with COOP, COEP, and CORP.

Security headers are not a monolith. They are enforced at different layers of the request path, by different actors, and under different caching semantics. Some headers — like Strict-Transport-Security — are remembered by the browser for months after a single response. Others — like a per-request Content-Security-Policy nonce — must be regenerated on every response or the policy is silently defeated. Treating all headers as one undifferentiated block of add_header lines is the single most common cause of misconfiguration. The phased strategy below sequences them by blast radius: transport first, then injection control, then framing, then privacy, then legacy cleanup, then telemetry.

Where each header class is enforced along the request path A request flows from browser through CDN and reverse proxy to the application; the diagram shows which security header class each layer is responsible for emitting and where the browser enforces it. Browser Enforces HSTS, CSP, framing, isolation CDN / Edge HSTS, static cache headers Reverse proxy Frame, privacy, legacy cleanup Application Per-request CSP nonce Response headers added on the way back to the browser Enforcement rule of thumb Set a header at the outermost layer that can produce it correctly, except per-response values (nonces) which must come from the application. Duplicate headers across layers are a top misconfiguration: browsers may apply the most restrictive, or reject the response outright.
Each header class is owned by the layer best positioned to emit it; per-response values such as CSP nonces must originate in the application.

Security Scope & Operational Boundaries

This reference covers HTTP response header configuration at the edge, reverse-proxy, and application levels, TLS 1.2/1.3 handshake enforcement that underpins transport headers, and the browser security-context controls those headers activate (framing, resource isolation, cross-origin policies). It establishes the order of deployment, the cross-platform syntax, and the verification commands that confirm each control is live.

It explicitly excludes application-layer input sanitization, output encoding, parameterized queries, database encryption at rest, secrets management, and network-perimeter firewall configuration. Header enforcement is a defense-in-depth control: a strict Content-Security-Policy reduces the impact of an XSS bug but does not fix the bug, and it does not protect against server-side injection, SSRF, or broken authentication. Headers constrain what a browser will do with a response; they have no effect on non-browser API clients, server-to-server traffic, or native mobile apps that do not run a web engine. Treat the controls here as the browser-facing surface of a broader security program, not the whole program.

Auditing and ongoing measurement are treated as a first-class concern, covered in depth in the security header auditing and compliance reference and its companion security header audit checklist. Platform-specific syntax beyond the Nginx and Apache examples here — including Cloudflare, IIS, Node/Express, Next.js, and Django/FastAPI — is documented in the server and platform implementation guides.

Core Threat Model

Security headers directly address well-documented attack vectors by constraining browser behavior and enforcing strict origin policies. Each row below maps a real-world attack class to the header that neutralizes or materially reduces it. The mitigation is only as strong as its configuration — an unsafe-inline script source, a short HSTS max-age, or a missing always flag can render the control cosmetic.

Threat Category Primary Attack Vector Header Mitigation
Protocol downgrade & MITM interception SSL stripping (sslstrip), forced HTTP fallback on first request, captive-portal injection, certificate spoofing on untrusted networks Strict-Transport-Security with includeSubDomains and preload
Client-side code injection Reflected, stored, and DOM-based XSS; malicious third-party script tampering; <base> tag hijacking; DOM clobbering Content-Security-Policy with nonces or hashes, X-Content-Type-Options: nosniff
UI redressing & clickjacking Transparent iframe overlay, opacity tricks, drag-and-drop credential theft, double-click hijacking X-Frame-Options and CSP frame-ancestors
Privacy & data exfiltration Referer leakage of paths and tokens to third parties, silent access to camera/microphone/geolocation, cross-site behavioral tracking Referrer-Policy and Permissions-Policy
Cross-origin data theft via side channels Spectre-class speculative-execution reads, cross-origin resource inclusion that primes side channels, window.opener tampering Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy (COOP/COEP/CORP)
Stale credential & cache exposure Sensitive responses persisting in shared/back-button caches after logout, session data retained on shared machines Cache-Control and Clear-Site-Data
Legacy protocol exploitation Deprecated header conflicts (X-XSS-Protection filter bugs), HPKP foot-guns causing self-DoS, server fingerprinting Header deprecation audit and migration — see deprecated headers and legacy browser support

The two recurring failure modes across all categories are partial coverage (the header is present on the document but missing on API, error, or redirect responses) and cache contamination (a stale or duplicated header is served from an intermediary). The always flag in Nginx and Apache, addressed in every phase below, exists specifically to close the partial-coverage gap.

Phased Deployment Strategy

Deploying security headers requires a structured, ordered approach to prevent service disruption while progressively hardening the application surface. Each phase is independently deployable and independently verifiable. Roll out one phase to a staging environment, verify it with the listed commands, then promote it before starting the next. Never deploy CSP enforcement and HSTS preload in the same change — if something breaks you must be able to attribute it.

Phase 1: Enforce HTTPS & Transport Security

Objective: Establish baseline TLS integrity and make protocol downgrade impossible after the first visit.

The foundation of every other header is an encrypted, authenticated transport. A Strict-Transport-Security (HSTS) header instructs the browser to refuse plaintext HTTP to your origin for the duration of max-age, eliminating the sslstrip window that exists on the very first navigation only if you also submit to the preload list. Start with a short max-age (for example 300) in staging, confirm no subdomain or mixed-content breakage, then raise it to two years and add preload only when every subdomain serves valid TLS. Preload is effectively irreversible on a useful timescale, so it is the last toggle you flip, not the first. Detailed max-age and includeSubDomains rollout sequencing lives in how to configure max-age and includeSubDomains for HSTS and the preload vs manual enforcement trade-offs.

HSTS is only honored over HTTPS, so the header on an HTTP response is ignored — you must also redirect HTTP to HTTPS with a 301. Set the header at the outermost TLS-terminating layer (your CDN or edge); on Cloudflare, enable HSTS under SSL/TLS → Edge Certificates rather than via a header rule, which keeps a single source of truth.

Server Configuration:

# In the server { } block that listens on 443
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# `always` emits the header on 4xx/5xx and redirect responses too, not just 200s.
# In the <VirtualHost *:443> block, requires mod_headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# `always` ensures error responses also carry the header.

Verification Steps:

Phase 2: Resource Isolation & Script Execution Control

Objective: Restrict where executable code and assets may originate and neutralize client-side injection.

A Content-Security-Policy is the highest-value and highest-risk header you will deploy. It defines an allowlist of sources per resource type, and a strict policy reduces a stored-XSS payload from “full account takeover” to “blocked script, console warning, violation report.” The non-negotiable goal is to remove 'unsafe-inline' from script-src, which requires either a per-request nonce (see generating CSP nonces per request) or hashes of known inline scripts (see hash-based CSP for inline scripts). For large applications with many third-party scripts, pair a nonce with strict-dynamic so trusted scripts may load their own dependencies without enumerating every CDN host.

The nonce must be unique per response and unguessable, which means it cannot be set by a static add_header line — it must come from the application layer, with the proxy passing it through. Pair CSP with X-Content-Type-Options: nosniff, which stops the browser from MIME-sniffing a response into an executable type and is a hard requirement for several CSP and CORB protections to work. Always deploy in Content-Security-Policy-Report-Only first and collect violations before enforcing.

Server Configuration:

# Nonce ($csp_nonce) is generated by the app and exposed to nginx; never hardcode it.
add_header Content-Security-Policy "default-src 'self'; script-src 'nonce-$csp_nonce' 'strict-dynamic'; object-src 'none'; base-uri 'self'; style-src 'self'; frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
# `always` keeps the policy on every response so injected error pages are covered too.
# Requires mod_headers; %{CSP_NONCE}e is set by the app via SetEnvIf / mod_setenvif.
Header always set Content-Security-Policy "default-src 'self'; script-src 'nonce-%{CSP_NONCE}e' 'strict-dynamic'; object-src 'none'; base-uri 'self'; style-src 'self'; frame-ancestors 'self'"
Header always set X-Content-Type-Options "nosniff"

Verification Steps:

Phase 3: Framing Restrictions & UI Redressing Prevention

Objective: Block unauthorized embedding of your pages to defeat clickjacking.

Clickjacking loads your page in an invisible iframe over attacker-controlled UI so victims click your buttons unknowingly. The modern control is the CSP frame-ancestors directive; the legacy control is X-Frame-Options. Set both: frame-ancestors is authoritative in every current browser and supports an explicit allowlist of permitted embedders, while X-Frame-Options: SAMEORIGIN covers older clients. Where the two disagree, modern browsers honor frame-ancestors. The differences and migration path are detailed in X-Frame-Options vs CSP frame-ancestors; if a legitimate partner integration breaks, see fixing X-Frame-Options blocking legitimate iframes.

X-Frame-Options has no multi-origin allowlist — the deprecated ALLOW-FROM value is unsupported — so any allowlist beyond same-origin must live in frame-ancestors. On IIS, set both via <httpProtocol><customHeaders> in web.config; on Cloudflare, prefer a Transform Rule so the value is consistent across cached and dynamic responses.

Server Configuration:

add_header X-Frame-Options "SAMEORIGIN" always;
# frame-ancestors belongs in the CSP from Phase 2; shown standalone here for clarity:
# add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example.com" always;
Header always set X-Frame-Options "SAMEORIGIN"
# Prefer extending the existing CSP with: frame-ancestors 'self' https://partner.example.com

Verification Steps:

Phase 4: Privacy Controls & Hardware Feature Delegation

Objective: Stop leaking referrer data and revoke browser hardware APIs you do not use.

By default, navigating from a secure page can leak the full URL — including path and query — in the Referer header to third parties, exposing session tokens, reset links, and internal structure. Referrer-Policy: strict-origin-when-cross-origin sends the full URL same-origin, only the origin cross-origin over HTTPS, and nothing on a downgrade to HTTP; the rationale and stricter alternatives are covered in setting Referrer-Policy strict-origin-when-cross-origin. Separately, Permissions-Policy lets you disable powerful features — camera, microphone, geolocation, payment, USB — for your own origin and all embedded frames, shrinking the attack surface a compromised third-party script can reach. Disable everything you do not actively use; see Permissions-Policy disabling browser features for the full feature list and allowlist syntax.

The empty-allowlist form feature=() disables a feature entirely, including for your own origin; feature=(self) keeps it for first-party use only. Mis-scoping here silently breaks embedded media players, payment widgets, and map components, so confirm what your front end actually calls before locking down.

Server Configuration:

add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=()" always;
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=()"

Verification Steps:

Phase 5: Legacy Header Audit & Deprecation Management

Objective: Remove headers that are deprecated, harmful, or that fingerprint your stack.

Older hardening advice still circulates, and some of it now actively hurts. X-XSS-Protection drove a browser filter that has been removed from Chrome and Edge and that, in its 1; mode=block form, introduced its own information-leak bugs — set it to 0 to disable any residual behavior so it cannot interfere with CSP. HPKP (Public-Key-Pins) is fully deprecated and a notorious self-DoS foot-gun; remove it entirely and never reintroduce it. Finally, strip identifying headers like Server and X-Powered-By that hand attackers your exact software versions. The full deprecation map and migration guidance is in deprecated headers and legacy browser support, and the safe removal procedure for fingerprinting headers is in removing X-Powered-By and Server headers safely.

Removing the Server token in Nginx requires the more_clear_headers directive from the third-party headers-more module (or server_tokens off to at least drop the version); in Apache use ServerTokens Prod and Header unset. On Cloudflare these tokens are largely normalized at the edge, but verify rather than assume.

Server Configuration:

# Disable the obsolete XSS filter so it can't conflict with CSP
add_header X-XSS-Protection "0" always;
# Never set Public-Key-Pins — it is deprecated and dangerous
server_tokens off;                 # drop the Nginx version from the Server header
# more_clear_headers "Server";     # full removal needs the headers-more module
Header always set X-XSS-Protection "0"
ServerTokens Prod                  # reduce the Server header to "Apache"
Header always unset X-Powered-By   # strip the app-server fingerprint
# Ensure no Header set Public-Key-Pins lines remain anywhere

Verification Steps:

Phase 6: Cross-Origin Isolation & Violation Telemetry

Objective: Opt into a stronger isolation tier where required, and capture policy violations continuously.

Two distinct hardening tasks complete the rollout. First, if your application needs high-resolution timers or SharedArrayBuffer — common for WebAssembly, video processing, or analytics — the browser gates them behind cross-origin isolation, which you enable with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. COOP severs the window.opener relationship to defeat cross-window attacks; COEP forces every embedded resource to explicitly opt in via CORP or CORS. This is a breaking change for any page that loads third-party resources without those headers — work through enabling cross-origin isolation for SharedArrayBuffer and keep debugging COEP-blocked resources handy. Even when you do not need isolation, set Cross-Origin-Opener-Policy: same-origin for its standalone clickjacking and cross-window protection.

Second, make violation telemetry permanent. Keep a Content-Security-Policy-Report-Only policy running alongside the enforcing one to catch regressions, and pipe both to a reporting endpoint via report-to. The endpoint setup and the report-urireport-to migration are covered in the CSP report-uri vs report-to migration guide, and a CI/CD scanning pipeline is described in automated header scanning in CI/CD.

Server Configuration:

add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;   # only when isolation is required
add_header Reporting-Endpoints 'csp-endpoint="https://example.com/csp-report"' always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-to csp-endpoint" always;
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Reporting-Endpoints "csp-endpoint=\"https://example.com/csp-report\""
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-to csp-endpoint"

Verification Steps:

Security Trade-offs & Operational Considerations

Strict headers create real, measurable tensions that you must resolve deliberately rather than accept by default. The dominant ones:

Resolve these by sequencing changes through staging with a short max-age, leaning on report-only telemetry, owning each header at exactly one layer, and validating continuously in CI rather than once at launch. Defense-in-depth requires the headers here to be one verified, monitored layer among many — not a launch-day checkbox.