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.

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)

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.