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_moduledirective is needed.
Module Page Links
- RPM packages: nginx-extras.getpagespeed.com/modules/security-headers
- APT packages: apt-nginx-extras.getpagespeed.com/modules/security-headers
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(sendsX-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
- Module not loaded: If
security_headers on;passesnginx -t, the module is loaded. - Wrong context: The directive must be in
http,server, orlocationcontext. - Directive order:
load_modulemust appear before thehttpblock.
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
- Always enable
hide_server_tokenson front-facing servers. Information disclosure helps attackers. -
Use HSTS preload cautiously. Once preloaded, you cannot revert to HTTP for any subdomain.
-
Do not enable COOP/COEP without testing. These headers can break OAuth and payment integrations.
-
Test before production. Verify headers work with popups, iframes, and third-party integrations.
-
Handle CSP separately. This module does not add Content-Security-Policy. CSP requires application-specific configuration.
Related Resources
- GitHub Repository: ngx_security_headers
- NGINX Security Headers: The Right Way – Security headers concepts
- NGINX Cookie Flag Module – Secure cookie configuration
- NGINX Headers More Module – Advanced header manipulation
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.
