yum upgrades for production use, this is the repository for you.
Active subscription is required.
đź“… Updated: January 27, 2026 (Originally published: August 5, 2018)
HTTP Strict Transport Security (HSTS) is a critical security header that protects your website visitors from man-in-the-middle attacks and SSL stripping. When configured correctly in NGINX, HSTS instructs browsers to only communicate with your server over HTTPS, even if a user types `http://` or clicks an HTTP link.
In this guide, we examine how HSTS works at the protocol level, why proper NGINX HSTS configuration requires careful attention to redirect patterns, and how to achieve HSTS preload eligibility. All configurations have been tested on Rocky Linux with NGINX 1.28.
What is HSTS and Why Does It Matter?
The Strict-Transport-Security header was standardized in RFC 6797. When a browser receives this header over an HTTPS connection, it remembers that the domain should only be accessed via HTTPS for a specified period. Understanding NGINX HSTS is essential for any administrator securing web applications.
The SSL Stripping Problem
Consider what happens when a user types example.com in their browser:
- The browser sends an HTTP request to `http://example.com`
- Your server redirects to `https://example.com`
- The browser follows the redirect and establishes an HTTPS connection
That initial HTTP request is vulnerable. An attacker positioned between the user and your server (on public WiFi, for example) can intercept the HTTP request and prevent the redirect. The attacker forwards the request to your server, receives the HTTPS content, and serves it to the user over HTTP.
The user sees your content without encryption, and the attacker captures everything—login credentials, session cookies, sensitive data. This attack is called SSL stripping, and HSTS is the primary defense against it.
How HSTS Protects Users
After receiving the HSTS header, browsers remember to use HTTPS for that domain. The next time a user types `http://example.com` or clicks an HTTP link, the browser internally converts the URL to HTTPS before sending any network request. No HTTP request ever leaves the browser.
This protection continues for the duration specified in the max-age directive. Setting max-age to one year (31536000 seconds) or two years (63072000 seconds) ensures protection between visits.
HSTS Header Syntax and Directives
The Strict-Transport-Security header has three directives:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
The max-age Directive
The max-age directive specifies how long (in seconds) the browser should remember to only use HTTPS. This value refreshes each time the browser receives the header.
Recommended values:
| Duration | Seconds | Use Case |
|---|---|---|
| 1 day | 86400 | Initial testing only |
| 1 week | 604800 | Extended testing |
| 6 months | 15768000 | Minimum for production |
| 1 year | 31536000 | Minimum for HSTS preload |
| 2 years | 63072000 | Recommended for production |
Setting max-age=0 disables HSTS and removes the policy from the browser cache. This must be sent over HTTPS to take effect.
The includeSubDomains Directive
When present, includeSubDomains extends the HSTS policy to all subdomains. If example.com sets HSTS with includeSubDomains, the policy also applies to www.example.com, api.example.com, mail.example.com, and any other subdomain.
This directive is required for HSTS preload and strongly recommended for all deployments. Without it, an attacker could target an unprotected subdomain to steal cookies scoped to the parent domain.
The preload Directive
The preload directive indicates your intent to submit the domain to browser preload lists. It is not part of RFC 6797 but is recognized by all major browsers.
When a domain is preloaded, browsers have it hardcoded in their source code. Users visiting for the first time will automatically use HTTPS, eliminating the trust-on-first-use vulnerability entirely.
NGINX HSTS Configuration
In NGINX, you add the HSTS header using the add_header directive. The critical requirement: HSTS headers must only be sent over HTTPS connections. Browsers ignore the header if received over HTTP.
Basic NGINX HSTS Setup
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# NGINX HSTS header - only on HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
root /var/www/html;
}
The always parameter ensures the header is sent for all response codes, including errors. Without it, NGINX only adds the header for successful responses (2xx and 3xx).
Checking Your Current NGINX HSTS Configuration
Verify that NGINX is configured with HSTS:
nginx -T 2>/dev/null | grep -i "strict-transport"
Test the actual response headers:
curl -sI https://example.com | grep -i "strict-transport"
Expected output:
strict-transport-security: max-age=63072000; includeSubDomains; preload
NGINX HSTS Redirect Patterns for www and non-www
The most common NGINX HSTS misconfiguration involves redirect patterns. If your canonical domain uses www (like www.example.com), you need two redirects to ensure both the apex domain and www subdomain receive the HSTS header.
Canonical www Domain (www.example.com)
When www.example.com is your primary domain, a visitor typing example.com must see the HSTS header for example.com before being redirected to www.example.com. Otherwise, future visits to `http://example.com` remain vulnerable.
The correct redirect flow:
http://example.com → https://example.com (with HSTS) → https://www.example.com (with HSTS)
Here is the complete NGINX HSTS configuration for www canonical domains:
# HTTP to HTTPS redirect for apex domain
server {
listen 80;
listen [::]:80;
server_name example.com;
# No HSTS here - browsers ignore it over HTTP
return 301 https://example.com$request_uri;
}
# HTTPS apex domain - redirect to www with HSTS
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# HSTS header for apex domain
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
return 301 https://www.example.com$request_uri;
}
# HTTP to HTTPS redirect for www
server {
listen 80;
listen [::]:80;
server_name www.example.com;
return 301 https://www.example.com$request_uri;
}
# HTTPS www - main server block
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# HSTS header for www
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
root /var/www/html;
index index.html;
}
Canonical non-www Domain (example.com)
When the apex domain is canonical and you use includeSubDomains, you can redirect directly from http://www.example.com` tohttps://example.com`. The includeSubDomains directive ensures the www subdomain inherits the HSTS policy from the apex.
# HTTP to HTTPS redirect for apex
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://example.com$request_uri;
}
# HTTP www to HTTPS apex (direct redirect)
server {
listen 80;
listen [::]:80;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
# HTTPS www - redirect to apex with HSTS
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# HSTS header (includeSubDomains covers www)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
return 301 https://example.com$request_uri;
}
# HTTPS apex - main server block
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# HSTS header with preload
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
root /var/www/html;
index index.html;
}
With includeSubDomains, once the browser has the HSTS policy for example.com, it automatically applies to all subdomains including www.example.com. This allows for a simpler redirect flow for non-www canonical domains.
Common NGINX HSTS Misconfigurations
Several NGINX configuration patterns break HSTS protection. The Gixy NGINX security analyzer can detect some of these issues automatically.
Missing always Parameter
Without always, NGINX only sends the header for 2xx and 3xx responses:
# Wrong - header missing on error pages
add_header Strict-Transport-Security "max-age=63072000";
# Correct - header sent for all responses
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Error pages without the HSTS header create a small window where browsers might not receive the policy update.
HSTS Header on HTTP Server Block
Sending HSTS over HTTP is pointless—browsers explicitly ignore it:
# Wrong - browsers ignore this
server {
listen 80;
server_name example.com;
add_header Strict-Transport-Security "max-age=63072000";
return 301 https://example.com$request_uri;
}
Only configure NGINX HSTS in server blocks listening on port 443 with SSL enabled.
NGINX add_header Inheritance
A critical NGINX behavior: add_header directives in a child context replace (not extend) headers from the parent context. If you add any header in a location block, you must redeclare HSTS:
server {
listen 443 ssl;
server_name example.com;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
location /api {
# This add_header REPLACES the parent headers
add_header X-API-Version "1.0";
# HSTS is now missing for /api/* requests!
}
}
The solution is to redeclare HSTS in the location block:
location /api {
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-API-Version "1.0";
}
Alternatively, consider using the ngx_headers_more module which provides more_set_headers with better inheritance behavior.
Short max-age Values
Using a short max-age leaves users unprotected between visits:
# Weak - only 1 hour protection
add_header Strict-Transport-Security "max-age=3600";
# Strong - 2 year protection
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
For production deployments, use at least one year (31536000 seconds). For HSTS preload, two years (63072000 seconds) is recommended.
Missing includeSubDomains
Without includeSubDomains, attackers can use insecure subdomains to set cookies for the parent domain:
# Vulnerable - subdomains unprotected
add_header Strict-Transport-Security "max-age=63072000";
# Secure - all subdomains protected
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
HSTS Preload List Submission
The HSTS preload list eliminates the trust-on-first-use vulnerability by hardcoding your domain in browser source code. All major browsers share this list.
Preload Requirements
To be accepted into the preload list at hstspreload.org, your domain must:
- Serve a valid SSL certificate
- Redirect HTTP to HTTPS on the same host
- Serve the HSTS header on the apex domain over HTTPS
- Include
max-ageof at least 31536000 seconds (1 year) - Include the
includeSubDomainsdirective - Include the
preloaddirective - All subdomains must support HTTPS
The minimum preload-ready header:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
The recommended header uses two years:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Preload Considerations
Before submitting to the preload list, understand the implications:
- Removal takes months. Once preloaded, removing your domain requires submitting a removal request and waiting for browser updates to propagate.
