NGINX Security Headers Deep Dive

Configure HSTS, CSP, X-Frame-Options, and other security headers to protect your users from XSS, clickjacking, and more.

Updated: January 2025 10 min read

Why Security Headers Matter

Security headers are HTTP response headers that instruct browsers to enable security features. They're your first line of defense against common web attacks like:

  • Cross-Site Scripting (XSS) — Injecting malicious scripts
  • Clickjacking — Tricking users into clicking hidden elements
  • MIME Sniffing — Browsers misinterpreting content types
  • Protocol Downgrade — Forcing HTTPS connections to HTTP
  • Information Leakage — Revealing sensitive data via referrer headers

Essential Security Headers

Header Purpose Priority
Strict-Transport-Security Force HTTPS connections Critical
Content-Security-Policy Prevent XSS and injection attacks Critical
X-Frame-Options Prevent clickjacking High
X-Content-Type-Options Prevent MIME sniffing High
Referrer-Policy Control referrer information Medium
Permissions-Policy Control browser features Medium

Strict-Transport-Security (HSTS)

HSTS tells browsers to only access your site via HTTPS, preventing protocol downgrade attacks and cookie hijacking.

# Recommended HSTS configuration
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Parameters Explained

  • max-age=31536000 — Remember for 1 year (in seconds)
  • includeSubDomains — Apply to all subdomains
  • preload — Allow inclusion in browser preload lists
  • always — Send header even on error responses

HSTS Preload Warning

Only use preload if you're certain all current and future subdomains will support HTTPS. Removing a domain from preload lists requires months.

Content-Security-Policy (CSP)

CSP is the most powerful header for preventing XSS. It defines which sources of content are allowed to load.

# Basic CSP - adjust based on your needs
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;

Key Directives

  • default-src — Fallback for other directives
  • script-src — Allowed JavaScript sources
  • style-src — Allowed CSS sources
  • img-src — Allowed image sources
  • connect-src — Allowed fetch/XHR/WebSocket targets
  • frame-ancestors — Who can embed your page (replaces X-Frame-Options)
Start with Report-Only: Use Content-Security-Policy-Report-Only first to test your policy without breaking functionality. Monitor the reports before enforcing.

X-Frame-Options

Prevents your site from being embedded in iframes, protecting against clickjacking attacks.

# Prevent all framing
add_header X-Frame-Options "DENY" always;

# Or allow same-origin framing
add_header X-Frame-Options "SAMEORIGIN" always;

Options

  • DENY — Never allow framing
  • SAMEORIGIN — Only allow framing from same origin

Note: frame-ancestors in CSP is more flexible and should be preferred for modern browsers.

X-Content-Type-Options

Prevents browsers from MIME-sniffing responses, which can turn non-executable content into executable code.

add_header X-Content-Type-Options "nosniff" always;

Referrer-Policy

Controls how much referrer information is sent when navigating to other sites.

# Recommended: send origin only for cross-origin requests
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Common Values

  • no-referrer — Never send referrer
  • same-origin — Only send referrer for same-origin requests
  • strict-origin-when-cross-origin — Full URL for same-origin, only origin for cross-origin (recommended)

Permissions-Policy

Controls which browser features (camera, microphone, geolocation, etc.) can be used.

add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always;

The add_header Inheritance Problem

NGINX's add_header directive has a critical gotcha: when you use add_header in a nested block (like a location), it replaces all headers from parent blocks.

# WRONG: location block drops server-level headers!
server {
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    location /api/ {
        add_header X-Custom "value" always;
        # X-Frame-Options and X-Content-Type-Options are GONE!
    }
}
# RIGHT: repeat all headers in nested blocks
server {
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    location /api/ {
        add_header X-Frame-Options "DENY" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Custom "value" always;
    }
}
NGINX 1.29.3+ Solution: Use add_header_inherit on; in nested blocks to automatically inherit parent headers.
location /api/ {
    add_header_inherit on;
    add_header X-Custom "value" always;
}

Complete Recommended Configuration

# Place in http block or server block
# Security headers for all responses

# HSTS - force HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Prevent clickjacking
add_header X-Frame-Options "DENY" always;

# Prevent MIME sniffing
add_header X-Content-Type-Options "nosniff" always;

# Control referrer
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Restrict browser features
add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always;

# CSP - customize based on your application
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none';" always;

Detecting Issues with Gixy

Gixy detects several header-related misconfigurations:

  • Missing HSTS — HTTPS servers without Strict-Transport-Security
  • Weak HSTS max-age — Values less than 6 months
  • Header redefinition — Nested blocks dropping parent headers
$ gixy /etc/nginx/nginx.conf

[hsts_header] Missing HSTS header
  Severity: MEDIUM
  Reason: No Strict-Transport-Security header found.

[add_header_redefinition] Nested "add_header" drops parent headers.
  Severity: MEDIUM
  Reason: Parent header(s) "x-frame-options" dropped in nested block

Testing Your Headers

After configuration, verify your headers:

# Check response headers
curl -I https://yourdomain.com

# Or use online tools:
# - securityheaders.com
# - observatory.mozilla.org

Further Reading