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