Skip to main content

NGINX / Security

NGINX Security Headers Module: Complete Configuration Guide

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Adding security headers to your NGINX configuration should be simple. Instead, most tutorials lead you into the add_header pitfall where headers mysteriously disappear when you add a nested location block. The NGINX security headers module eliminates this problem entirely. It adds intelligent header management that manual configuration cannot match.

This guide covers the ngx_security_headers module in depth. You will learn every directive, every option, and the security implications of each choice.

Why Use This Module Instead of Manual Headers?

Before diving into configuration, understand why this module exists. Manual security header configuration in NGINX has three fundamental problems.

The add_header Inheritance Problem

The add_header directive does not inherit into nested contexts. If you define headers in a server block and then add any add_header in a location block, all your server-level headers vanish:

server {
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    location /api {
        add_header X-API-Version "1.0";
        # X-Frame-Options and X-Content-Type-Options are GONE here
    }
}

This behavior has caused countless security misconfigurations. The module operates as a filter and adds headers regardless of location nesting.

Content-Type Awareness

Sending X-Frame-Options for a CSS file wastes bandwidth. It also violates the principle of minimal headers. The module automatically applies frame-related headers only to HTML content types. It does not add them to images, scripts, or stylesheets.

Conditional GET Handling

When a client sends If-Modified-Since and receives a 304 Not Modified response, there is no response body. The module intelligently skips headers that only matter for full responses. This reduces unnecessary header transmission.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-security-headers

Then load the module in /etc/nginx/nginx.conf:

load_module modules/ngx_http_security_headers_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-security-headers

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

Quick Start Configuration

Enable the NGINX security headers module with sensible defaults:

http {
    security_headers on;
    hide_server_tokens on;

    server {
        listen 443 ssl;
        server_name example.com;
        # ... your configuration
    }
}

This single configuration adds the following headers to HTML responses:

Header Value
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
Cross-Origin-Resource-Policy same-site
Strict-Transport-Security max-age=31536000; includeSubDomains; preload

It also removes the Server header and strips over 20 other headers that leak software information.

Configuration Directives Reference

security_headers

  • Syntax: security_headers on | off
  • Default: off
  • Context: http, server, location

This is the master switch. When enabled, the module adds the default set of security headers and enables all other security_headers_* directives.

http {
    security_headers on;

    server {
        listen 80;
        # Headers apply here

        location /legacy {
            security_headers off;
            # Headers disabled for legacy endpoints
        }
    }
}

hide_server_tokens

  • Syntax: hide_server_tokens on | off
  • Default: off
  • Context: http, server, location

When enabled, this directive removes headers that reveal software information. It goes far beyond the built-in server_tokens off directive. That directive only affects the Server header version number.

The hide_server_tokens on directive removes these headers completely:

Header Source
Server NGINX itself
X-Powered-By PHP, Node.js, etc.
X-CF-Powered-By Cloudflare
Via Proxy servers
X-Amz-CF-ID AWS CloudFront
X-Amz-CF-Pop AWS CloudFront
X-Page-Speed mod_pagespeed
X-Varnish Varnish Cache
X-Cache Various caches
X-Cache-Hits Various caches
X-Cache-Status Various caches
X-Application-Version Application frameworks
X-Hudson Jenkins/Hudson
X-Jenkins Jenkins
X-Envoy-Upstream-Service-Time Envoy proxy
X-Drupal-Cache Drupal CMS
X-Generator CMS platforms
X-Backend-Server Load balancers

Important: Some headers serve functional purposes. For example, X-Page-Speed prevents infinite loops when PageSpeed fetches resources. Enable hide_server_tokens only on front-facing NGINX instances that serve browsers directly.

# Front-facing NGINX serving browsers
server {
    listen 443 ssl;
    security_headers on;
    hide_server_tokens on;  # Safe here
}

security_headers_xss

  • Syntax: security_headers_xss off | on | block | omit
  • Default: off (sends X-XSS-Protection: 0)
  • Context: http, server, location

Controls the X-XSS-Protection header. This header is deprecated. The module sends 0 by default to disable it.

Value Header Sent Recommendation
off X-XSS-Protection: 0 Recommended. Disables the broken XSS filter.
on X-XSS-Protection: 1 Not recommended. Filter has known bypasses.
block X-XSS-Protection: 1; mode=block Not recommended. Can introduce vulnerabilities.
omit (none) Use if your upstream application sends this header.

Why the default sends 0: The XSS filter in older browsers is deprecated. It can introduce XSS vulnerabilities. Modern browsers have removed this feature entirely.

security_headers_frame

  • Syntax: security_headers_frame sameorigin | deny | omit
  • Default: sameorigin
  • Context: http, server, location

Controls the X-Frame-Options header. This header prevents clickjacking attacks by restricting how your pages can be embedded.

Value Header Sent Use Case
sameorigin X-Frame-Options: SAMEORIGIN Allow embedding only from the same origin
deny X-Frame-Options: DENY Prevent all embedding, even same-origin
omit (none) Let upstream application control this header
# Banking/sensitive pages - deny all embedding
location /account {
    security_headers on;
    security_headers_frame deny;
}

# Embeddable widgets - disable frame protection
location /widget {
    security_headers on;
    security_headers_frame omit;
}

security_headers_referrer_policy

  • Syntax: security_headers_referrer_policy <policy> | omit
  • Default: strict-origin-when-cross-origin
  • Context: http, server, location

Controls the Referrer-Policy header. This header determines how much referrer information is sent with requests.

Policy Behavior
no-referrer Never send referrer
no-referrer-when-downgrade Send full URL, except for HTTPS→HTTP
same-origin Send referrer only to same origin
origin Send only the origin (no path)
strict-origin Send origin only, except for HTTPS→HTTP
origin-when-cross-origin Full URL to same origin, origin only cross-origin
strict-origin-when-cross-origin Default. Balanced privacy and functionality
unsafe-url Always send full URL (not recommended)
omit Do not send this header

security_headers_hsts_preload

  • Syntax: security_headers_hsts_preload on | off
  • Default: on
  • Context: http, server, location

Controls whether preload is included in the Strict-Transport-Security header.

With preload (default): max-age=31536000; includeSubDomains; preload
Without preload: max-age=31536000; includeSubDomains

Critical warning: When preload is included, your domain becomes eligible for browser preload lists. Browsers will refuse HTTP connections to your domain permanently. This is difficult to undo.

# Conservative - HSTS without preload
server {
    listen 443 ssl;
    security_headers on;
    security_headers_hsts_preload off;
}

Important: The module only sends HSTS for HTTPS requests. HTTP requests never receive the HSTS header. This is correct behavior per RFC 6797.

security_headers_corp

  • Syntax: security_headers_corp same-site | same-origin | cross-origin | omit
  • Default: same-site
  • Context: http, server, location

Controls the Cross-Origin-Resource-Policy (CORP) header. This header tells browsers whether other origins can load your resources.

Value Behavior
same-site Default. Resources loadable by same-site origins
same-origin Resources only loadable by exact same origin
cross-origin Resources loadable by any origin
omit Do not send this header

security_headers_coop

  • Syntax: security_headers_coop same-origin | same-origin-allow-popups | unsafe-none | omit
  • Default: omit
  • Context: http, server, location

Controls the Cross-Origin-Opener-Policy (COOP) header. This header manages window opener relationships across origins.

Why the default is omit: Enabling COOP can break popup communication patterns. Many applications rely on these for OAuth flows and payment windows.

security_headers_coep

  • Syntax: security_headers_coep require-corp | credentialless | unsafe-none | omit
  • Default: omit
  • Context: http, server, location

Controls the Cross-Origin-Embedder-Policy (COEP) header. This header restricts embedding of cross-origin resources.

Why the default is omit: Enabling COEP breaks third-party resources without CORS headers. This includes analytics, CDN assets, and fonts.

Cross-Origin Isolation

To enable cross-origin isolation, configure all three headers:

location /isolated-app {
    security_headers on;
    security_headers_corp same-origin;
    security_headers_coop same-origin;
    security_headers_coep require-corp;
}

Warning: This breaks cross-origin resources without CORS. Test thoroughly before deploying.

security_headers_text_types

  • Syntax: security_headers_text_types mime-type ...
  • Default: text/html application/xhtml+xml text/xml text/plain
  • Context: http, server, location

Specifies which MIME types receive HTML-specific security headers like X-Frame-Options.

Complete Configuration Examples

Standard Website

load_module modules/ngx_http_security_headers_module.so;

http {
    security_headers on;
    hide_server_tokens on;

    server {
        listen 443 ssl http2;
        server_name example.com;

        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        root /var/www/example.com;
        index index.html;
    }
}

API Server with Relaxed CORS

load_module modules/ngx_http_security_headers_module.so;

http {
    security_headers on;
    hide_server_tokens on;

    server {
        listen 443 ssl http2;
        server_name api.example.com;

        location /v1 {
            security_headers on;
            security_headers_frame omit;
            security_headers_corp cross-origin;

            proxy_pass http://backend;
        }
    }
}

Verifying Your Configuration

Test with curl

curl -sI https://example.com | grep -iE "(x-frame|x-content|referrer|strict-transport|cross-origin)"

Expected Output

X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Resource-Policy: same-site
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Online Testing Tools

Troubleshooting

Headers Not Appearing

  1. Module not loaded: If security_headers on; passes nginx -t, the module is loaded.
  2. Wrong context: The directive must be in http, server, or location context.
  3. Directive order: load_module must appear before the http block.

HSTS Not Sent on HTTP

This is correct behavior. The module only sends HSTS over HTTPS. This prevents downgrade attacks per RFC 6797.

Conflicting Headers from Upstream

Use omit for headers your application manages:

location /app {
    security_headers on;
    security_headers_frame omit;
    security_headers_referrer_policy omit;
}

Performance Considerations

The module operates as a header filter with minimal overhead:

  • No regex matching: Values are set from pre-compiled strings
  • Conditional processing: Headers are skipped for 304 responses
  • Content-type checking: HTML headers only added for configured MIME types

Module vs. Manual Configuration Comparison

Feature ngx_security_headers Module Manual add_header
Inheritance Works across nested contexts Breaks in nested contexts
Content-type awareness Automatic Manual implementation
304 handling Automatic Manual implementation
Header hiding 20+ headers automatically Manual per-header
HSTS on HTTP Automatically skipped Must implement manually
Configuration 2 lines 20+ lines

Security Best Practices

  1. Always enable hide_server_tokens on front-facing servers. Information disclosure helps attackers.

  2. Use HSTS preload cautiously. Once preloaded, you cannot revert to HTTP for any subdomain.

  3. Do not enable COOP/COEP without testing. These headers can break OAuth and payment integrations.

  4. Test before production. Verify headers work with popups, iframes, and third-party integrations.

  5. Handle CSP separately. This module does not add Content-Security-Policy. CSP requires application-specific configuration.

Conclusion

The NGINX security headers module transforms security header management from error-prone manual configuration into reliable automated protection. With security_headers on; and hide_server_tokens on;, you get comprehensive security headers that work correctly across nested locations.

The module respects content types and handles conditional requests properly. For most websites, the default configuration provides excellent security. Use the directive-specific options only when your application requires different settings.

Install the module today and eliminate the add_header inheritance problem forever.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.