Security Header Audit Checklist

This is the working checklist for the Security Header Auditing & Compliance reference: a single page an engineer runs top-to-bottom against a live origin to confirm every response header is present, correctly valued, and emitted on error pages as well as on 200 OK. It exists because header policy decays. A header set that scored an A six months ago drifts as someone adds a location block that drops inherited headers, a CDN strips a directive, a framework upgrade changes a default, or an error page returns from a path that never sees the middleware. The audit below is the standing control against that drift — each row maps a header to its required value, the attack it stops, the exact command that proves it, and the deep-dive reference where the full directive grammar lives.

Threat Model & Why an Audit

A point-in-time configuration review proves nothing about production. Three failure classes account for nearly every regression an audit catches, and none of them announce themselves:

The audit treats every header as a control with a verifiable wire value. You do not trust the config file; you read the bytes off the response. The coverage you are actually checking is three-dimensional — present, correctly valued, and present on error responses — and a header that satisfies the first two but fails the third is still a finding.

Header coverage matrix across three audit dimensions A matrix listing core security headers down the left and three audit checks across the top: present, correct value, and present on error pages, illustrating that all three must pass. Header Present? Correct? On 5xx? HSTS ok ok fail CSP ok weak ok Frame controls ok ok ok X-Content-Type fail fail fail Referrer-Policy ok ok ok A row passes only when all three columns pass. Drift hides in the third column.
Each header must pass three checks; an audit that stops at "present" misses drift on error pages and weak values.

The Checklist

Work this table top to bottom against the target origin. The Verify command column assumes you have set H=https://example.com in your shell; each command should print the expected value or nothing (a finding). The Deep dive column links to the reference with the full directive grammar, platform syntax, and rollback procedure for that header. Treat an empty result as a failed row, not a pass.

# Header Required value Why it matters Verify command Deep dive
1 Strict-Transport-Security max-age=63072000; includeSubDomains; preload Forces HTTPS, defeats SSL-stripping on every revisit and across subdomains. curl -sI $H | grep -i strict-transport-security HSTS deep dive
2 Content-Security-Policy default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none' (nonce or hash for scripts) Neutralizes XSS by restricting which scripts, styles, and frames execute. curl -sI $H | grep -i content-security-policy Content Security Policy essentials
3 X-Frame-Options DENY (legacy mirror of frame-ancestors) Blocks clickjacking in engines that predate frame-ancestors. curl -sI $H | grep -i x-frame-options Frame controls & X-Frame-Options
4 CSP frame-ancestors frame-ancestors 'none' (or an explicit allowlist) The modern, spec-driven framing control; supersedes X-Frame-Options. curl -sI $H | grep -io "frame-ancestors[^;]*" X-Frame-Options vs frame-ancestors
5 X-Content-Type-Options nosniff Stops MIME sniffing that turns an uploaded file into executable script. curl -sI $H | grep -i x-content-type-options Content Security Policy essentials
6 Referrer-Policy strict-origin-when-cross-origin (or no-referrer) Prevents leaking full URLs (tokens, paths) to third-party origins. curl -sI $H | grep -i referrer-policy Referrer-Policy & Permissions-Policy
7 Permissions-Policy geolocation=(), camera=(), microphone=(), interest-cohort=() Disables powerful browser features the site does not use, shrinking attack surface. curl -sI $H | grep -i permissions-policy Permissions-Policy: disabling features
8 Cross-Origin-Opener-Policy same-origin Severs the window.opener link, isolating the browsing context group. curl -sI $H | grep -i cross-origin-opener-policy Cross-origin isolation: COOP/COEP/CORP
9 Cross-Origin-Embedder-Policy require-corp (only when isolation is needed) Required for cross-origin isolation; gates SharedArrayBuffer and precise timers. curl -sI $H | grep -i cross-origin-embedder-policy Enabling cross-origin isolation
10 Cross-Origin-Resource-Policy same-origin (or same-site) Stops other origins embedding your resources; mitigates Spectre-class leaks. curl -sI $H | grep -i cross-origin-resource-policy Cross-origin isolation: COOP/COEP/CORP
11 Cache-Control (sensitive pages) no-store on authenticated/sensitive responses Keeps secrets out of shared caches and the back/forward cache. curl -sI $H/account | grep -i cache-control Cache-Control & Clear-Site-Data
12 Clear-Site-Data (logout only) "cache", "cookies", "storage" on the logout response Purges client-side state at logout so a shared device retains no session. curl -sI $H/logout | grep -i clear-site-data Using Clear-Site-Data on logout
13 X-Powered-By absent A removed banner denies attackers cheap framework/version fingerprinting. curl -sI $H | grep -i x-powered-by (expect no output) Removing X-Powered-By and Server headers
14 Server minimized (e.g. nginx, no version) A version-stripped Server value slows targeted CVE reconnaissance. curl -sI $H | grep -i "^server:" Removing X-Powered-By and Server headers
15 Legacy X-XSS-Protection 0 or absent The legacy auditor is deprecated and itself exploitable; disable, do not enable. curl -sI $H | grep -i x-xss-protection Deprecated headers & legacy support

For every row that passes on the homepage, repeat the command against a 404 (curl -sI $H/this-path-does-not-exist) and, where you can induce one, a 5xx. A header that is present on 200 but absent on 404 is the single most common drift finding — the always flag covers it on Nginx and Apache, and the full per-platform syntax is in each linked reference. The implementation patterns for the whole set are consolidated in the Web Security Headers Fundamentals reference and applied per stack in the Server & Platform Implementation Guides.

Platform-Specific Implementation

Below is a known-good baseline for the full header set, ready to paste and then tighten. Emit headers at the edge or reverse proxy so they cover error pages and responses served while the application is down. The CSP shown is a starting skeleton — replace 'nonce-...' with a per-request nonce as described in Content Security Policy essentials before treating it as production-grade.

Nginx

server {
    listen 443 ssl;
    server_name example.com;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; upgrade-insecure-requests" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), interest-cohort=()" always;
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Cross-Origin-Resource-Policy "same-origin" always;

    # Strip fingerprinting banners
    server_tokens off;            # removes the nginx version from Server
    proxy_hide_header X-Powered-By;
    more_clear_headers "X-Powered-By";   # requires headers-more-nginx-module
}

The always flag is mandatory on every add_header: without it Nginx omits the header on 4xx/5xx responses, leaving exactly the error pages an attacker can induce unprotected. The critical trap is inheritance — declaring any add_header inside a location block discards all headers inherited from server, so you must re-declare the full set in any location that adds its own. Detail in the Nginx security headers configuration reference. Verify: curl -sI https://example.com.

This inheritance rule is worth picturing, because it is the trap most audits fail to predict. Nginx add_header directives are not additive across contexts: a child context that declares even one add_header starts with an empty header table and inherits nothing from its parent. So a location that adds a single caching or CORS header silently strips the entire security set the server block declared — the config still reads correctly, but the response for that one path is naked. The figure below shows the same server block feeding two locations with opposite outcomes.

Nginx add_header inheritance reset in location blocks A server block declaring eight security headers feeds two locations; the one with no add_header inherits all of them while the one that adds its own header drops the entire inherited set. server { ... } declares 8 headers via add_header ... always inherit own add_header location /api/ no add_header of its own inherits all 8 headers response fully covered location /legacy/ adds one add_header X-Extra 8 inherited headers dropped only X-Extra is sent
An Nginx child context that declares any add_header inherits none: the /legacy/ location loses the whole security set the server block sent.

Apache

<VirtualHost *:443>
    ServerName example.com

    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; upgrade-insecure-requests"
    Header always set X-Frame-Options "DENY"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "geolocation=(), camera=(), microphone=(), interest-cohort=()"
    Header always set Cross-Origin-Opener-Policy "same-origin"
    Header always set Cross-Origin-Resource-Policy "same-origin"

    # Strip fingerprinting banners
    Header always unset X-Powered-By
    ServerTokens Prod          # Server: Apache (no version)
    ServerSignature Off
</VirtualHost>

always is the condition argument to set: it ensures the header is appended even on internally generated error documents, where the default onsuccess table skips them. Requires mod_headers (apachectl -M | grep headers). ServerTokens and ServerSignature belong in the main server config, not per-directory. Full patterns in the Apache .htaccess and VirtualHost hardening reference. Verify: curl -sI https://example.com.

Verification & Diagnostic Workflows

One command dumps every header for a fast visual pass — the same quick-check pattern used on the home page, widened to the full audit set:

curl -sI https://example.com | grep -iE 'strict-transport-security|content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy|cross-origin-(opener|embedder|resource)-policy|cache-control|clear-site-data|x-powered-by|^server:'

Then confirm the headers survive an error response, because that is where drift lives:

curl -sI https://example.com/this-path-404s | grep -iE 'strict-transport-security|content-security-policy|x-frame-options'
# All three should still print. Empty output is a finding.

Validate the TLS chain before trusting Strict-Transport-Security — HSTS hard-fails on any certificate error, so a SAN gap on a subdomain is a lockout waiting to happen:

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
  | grep -E 'Verify return code|subject='
# Verify return code: 0 (ok)

These commands are not four unrelated checks; they are an escalating pipeline in which each stage exposes a gap the previous stage is structurally blind to. The homepage dump proves presence and value on the happy path. The 404 re-run proves the always flag actually fired on a response the middleware never touched. The openssl step proves the transport HSTS depends on is sound before you trust a max-age you cannot easily revoke. Only then does an external grade mean anything — run it last, as a witness, not as the audit itself.

Escalating verification pipeline for a header audit Five sequential audit stages from dumping headers on a 200 response through matching values, re-running on error pages, validating the TLS chain, and finishing with an external scanner. Each stage catches a failure the previous one cannot see curl -sI on 200 match value vs required re-run on 404 / 5xx openssl verify chain scanner second opinion Empty output at any stage is a finding, not a pass. Validate the TLS chain before you trust an HSTS max-age you cannot easily revoke.
The audit is a pipeline: presence, value, error-page coverage, transport soundness, and an external witness — run in that order.

For a graded report and a second opinion, run the origin through an external scanner. The grade letters and what they reward (and the gaps they ignore) are explained in header grading with the Observatory and securityheaders, and the full local command toolkit — curl, openssl, and testssl.sh — is documented in auditing headers with curl, openssl, and testssl. Treat the external grade as a sanity check, not the source of truth: scanners test one URL, the audit table tests your error pages too.

Troubleshooting & Safe Rollback

Frequently Asked Questions

Which headers belong in a minimum baseline versus a hardened one? The minimum baseline that every site should pass is Strict-Transport-Security, Content-Security-Policy, a framing control (X-Frame-Options plus CSP frame-ancestors), X-Content-Type-Options: nosniff, and Referrer-Policy. A hardened profile adds Permissions-Policy, the Cross-Origin-* isolation trio, Cache-Control: no-store on sensitive responses, Clear-Site-Data at logout, and removal of X-Powered-By plus a minimized Server.

Why does the audit insist on checking error pages? Because security headers are a property of the response, not the route, and 404/500/maintenance responses frequently bypass the application middleware that sets them. An attacker who can force an error then receives an unprotected document. The always flag on Nginx and Apache is the fix; the audit re-runs each command against a 404 to prove it.

Is X-Frame-Options redundant now that CSP has frame-ancestors? Keep both. frame-ancestors is the modern, spec-driven control and takes precedence in browsers that support it, but X-Frame-Options: DENY still covers older engines and is cheap to send. Set them to equivalent policies so they cannot disagree.

Should I enable X-XSS-Protection? No. The legacy XSS auditor is deprecated, removed from modern browsers, and was itself a source of information-disclosure vulnerabilities. If the header appears at all, set it to 0. Real XSS defense is a strict Content-Security-Policy.

How often should this checklist run? On every deployment as an automated gate, and on a schedule against production. Drift is introduced by routine config and dependency changes, so a once-a-quarter manual review is not enough — wire the table’s assertions into CI as covered in automated header scanning in CI/CD.