Nginx Security Headers Configuration
Threat Model & Header Strategy
Modern web applications require a layered defense strategy to mitigate client-side vulnerabilities. HTTP security headers operate at the transport and presentation layers, instructing browsers to enforce strict rendering boundaries, restrict resource loading, and prevent protocol downgrade attacks. Implementing a robust Server & Platform Implementation Guides framework ensures headers are deployed consistently across infrastructure tiers, eliminating configuration drift between staging and production environments.
Engineers must map headers directly to OWASP Top 10 attack vectors:
- Clickjacking & UI Redressing: Mitigated via
X-Frame-OptionsandContent-Security-Policy: frame-ancestors. - MIME-Type Sniffing & Drive-by Downloads: Blocked via
X-Content-Type-Options: nosniff. - Cross-Site Scripting (XSS) & Data Exfiltration: Controlled via
Content-Security-PolicyandReferrer-Policy. - Protocol Downgrade & MITM: Enforced via
Strict-Transport-Security.
Legacy Compatibility Trade-offs: Strict enforcement (e.g., preload HSTS, restrictive CSP) breaks functionality in legacy browsers (IE11, Android Browser <4.4, Safari <10). Agencies supporting enterprise legacy stacks must implement progressive enhancement: deploy Content-Security-Policy-Report-Only alongside permissive fallbacks, and audit user-agent traffic before committing to always enforcement.
Core Nginx Directive Syntax & Placement
Understanding directive inheritance prevents silent header drops. Nginx’s add_header directive follows strict inheritance rules: if a child block (server or location) defines any add_header, all parent-level headers are overridden unless explicitly redeclared. The always parameter guarantees injection on all response codes (2xx, 3xx, 4xx, 5xx), which is critical for error pages that otherwise bypass security policies. Upstream proxy behavior requires careful filtering to prevent duplicate or conflicting headers. For detailed precedence mapping, consult the Nginx add_header vs proxy_hide_header explained reference.
http {
# Global baseline headers applied to all virtual hosts
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
Security Impact: Establishes a baseline defense against UI redressing, MIME-type confusion, and referrer leakage across all virtual hosts. The always flag ensures error responses (403, 404, 500) retain security posture, preventing attackers from exploiting unhardened fallback pages.
Verification:
# Test standard response
curl -sI -o /dev/null -w "%{http_code}\n" https://your-domain.com
# Test error response (ensure headers persist)
curl -sI https://your-domain.com/nonexistent-page | grep -E "X-Frame-Options|X-Content-Type-Options|Referrer-Policy"
Step-by-Step Header Implementation
Deploy Strict-Transport-Security with calibrated max-age values to enforce HTTPS. Start with max-age=31536000 (1 year) and incrementally increase. Implement Content-Security-Policy using report-only mode before switching to enforce, allowing you to capture violations via your reporting endpoint without breaking production traffic. Map Permissions-Policy to disable unused browser APIs that increase attack surface. While origin-level configuration provides granular control, teams migrating from legacy stacks should review Apache .htaccess & VirtualHost Hardening for equivalent directive mappings.
server {
listen 443 ssl http2;
server_name your-domain.com;
# HSTS: Enforce HTTPS, include subdomains, prepare for preload submission
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# CSP: Progressive enforcement (start with report-only in staging)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none';" always;
# Permissions-Policy: Restrict hardware/browser features
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
}
Security Impact:
- HSTS eliminates SSL stripping attacks and forces secure connections.
- CSP blocks unauthorized script execution, mitigating DOM-based XSS and data exfiltration.
- Permissions-Policy reduces browser API abuse vectors, limiting malicious iframe or script access to hardware sensors.
Verification:
# Verify HSTS header presence and directives
curl -sI https://your-domain.com | grep -i strict-transport-security
# Validate CSP syntax and report-uri delivery (if configured)
curl -sI https://your-domain.com | grep -i content-security-policy
# Confirm Permissions-Policy restrictions
curl -sI https://your-domain.com | grep -i permissions-policy
CDN & Edge Proxy Compatibility
Reverse proxies behind CDNs frequently trigger duplicate header warnings or policy conflicts. When Nginx acts as an origin behind an edge network, upstream headers may conflict with edge-injected values. Use proxy_hide_header to strip upstream directives before injecting edge-optimized values. When offloading header management to external networks, align Nginx rules with Cloudflare Page Rules & Headers to prevent override loops and ensure consistent policy application across cached and dynamic responses.
location /api/ {
proxy_pass http://backend_upstream;
# Strip upstream headers that conflict with origin security policy
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
proxy_hide_header X-Frame-Options; # Prevent duplicate if CDN injects it
# Re-inject hardened headers for API responses
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
}
Security Impact: Eliminates header duplication that can cause browsers to apply the least restrictive policy. Stripping X-Powered-By and Server reduces information leakage. Enforcing Cache-Control: no-store on API endpoints prevents sensitive data from persisting in proxy or browser caches.
Verification:
# Inspect raw headers from the origin (bypass CDN cache)
curl -sI -H "Cache-Control: no-cache" https://your-domain.com/api/endpoint
# Check for duplicate headers (should appear exactly once per directive)
curl -sI https://your-domain.com/api/endpoint | grep -c "X-Frame-Options"
Verification & Diagnostic Workflows
Validate configurations using curl and OpenSSL to inspect raw HTTP responses. Cross-reference browser DevTools for header presence on cached and uncached assets. Run automated scanners to quantify compliance scores and identify missing directives before production rollout.
Command-Line Validation:
# Full header dump with TLS handshake verification
echo | openssl s_client -connect your-domain.com:443 -servername your-domain.com 2>/dev/null | openssl x509 -noout -dates
# Extract only security headers
curl -sI https://your-domain.com | grep -Ei "strict-transport|content-security|permissions-policy|x-frame|x-content-type|referrer-policy"
Browser DevTools Inspection:
- Open Network tab → Disable cache → Reload page.
- Inspect
200 OKand304 Not Modifiedresponses. - Verify headers persist on static assets (CSS/JS/images) served from the same origin.
- Check
Securitytab for mixed-content warnings or CSP violations.
Automated Scanner Integration:
- Mozilla Observatory:
observatory.mozilla.org→ Scores configuration against industry baselines. - SecurityHeaders.io:
securityheaders.com→ Validates header presence, syntax, and precedence. - CI/CD Pipeline Hook: Integrate
header-checkscripts into deployment pipelines to block merges if critical headers drop below threshold.
Common Misconfigurations & Troubleshooting
Missing always Flag on Error Pages
Symptom: Security headers appear on 200 OK but vanish on 403, 404, or 500.
Fix: Append always to every add_header directive. Without it, Nginx only injects headers on 2xx/3xx responses.
Verify: curl -sI https://your-domain.com/nonexistent | grep -i x-frame-options
CSP Breaking Inline Assets
Symptom: Console errors: Refused to execute inline script because it violates the following Content Security Policy directive.
Fix: Replace 'unsafe-inline' with cryptographic nonces ('nonce-<random>') or strict hashes. For legacy third-party widgets, temporarily scope CSP to specific paths via location blocks rather than global server directives.
Verify: Monitor CSP violation reports at your configured report-uri endpoint.
HSTS Upgrade Loops
Symptom: Browser stuck in redirect loop or fails to load HTTP-only resources.
Fix: Ensure all subdomains, CDNs, and internal APIs are accessible over HTTPS before submitting to the HSTS preload list. Remove preload if internal monitoring tools rely on HTTP. Verify includeSubDomains matches your actual DNS topology.
Verify: curl -sI -L http://your-domain.com | grep -i "location\|strict-transport"
proxy_hide_header Stripping Security Directives
Symptom: Upstream security headers disappear after passing through Nginx proxy.
Fix: Audit proxy_hide_header rules. Only strip informational headers (Server, X-Powered-By, X-AspNet-Version). Never strip Strict-Transport-Security or Content-Security-Policy unless explicitly overriding them downstream.
Verify: Compare upstream response (curl -sI http://backend_ip/) with proxied response to identify stripped directives.