Site icon GetPageSpeed

NGINX HSTS: Complete Strict-Transport-Security Guide

NGINX HSTS Configuration: Complete Strict-Transport-Security Guide

đź“… 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:

  1. The browser sends an HTTP request to `http://example.com`
  2. Your server redirects to `https://example.com`
  3. 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:

  1. Serve a valid SSL certificate
  2. Redirect HTTP to HTTPS on the same host
  3. Serve the HSTS header on the apex domain over HTTPS
  4. Include max-age of at least 31536000 seconds (1 year)
  5. Include the includeSubDomains directive
  6. Include the preload directive
  7. 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:

  • All subdomains are affected. The includeSubDomains requirement means every subdomain—including internal, development, and nested subdomains—must support HTTPS.

  • Existing HTTP services break. If you have legacy services running on HTTP subdomains, they will become inaccessible to users whose browsers have the preload list.

  • For large organizations with complex infrastructure, audit all subdomains before enabling HSTS preload.

    Double Redirect Performance

    Some administrators worry about the double redirect required for www canonical domains. In practice, the performance impact is negligible.

    For returning visitors: The browser already has the HSTS policy cached. When they type example.com, the browser internally converts to https://example.com`. The subsequent redirect tohttps://www.example.com` is the only actual HTTP round trip—identical to what happens without HSTS.

    For preloaded domains: No http://` tohttps://` redirect occurs at all. The browser knows to use HTTPS from the start, so visitors experience only the canonical redirect.

    Redirect responses are fast. The 301 redirect response is minimal—no page rendering, no asset loading. Modern browsers handle redirects efficiently.

    The security benefit of proper NGINX HSTS configuration far outweighs any concern about an additional redirect.

    Verifying NGINX HSTS Configuration with Gixy

    Gixy is a security analyzer for NGINX configurations. It detects NGINX HSTS misconfigurations among many other security issues.

    Install Gixy on Rocky Linux or AlmaLinux:

    dnf install gixy
    

    Analyze your NGINX configuration:

    gixy /etc/nginx/nginx.conf
    

    Gixy checks for missing HSTS headers on HTTPS servers and weak max-age values. A properly configured NGINX HSTS setup produces no HSTS-related warnings.

    For comprehensive NGINX security auditing, run Gixy as part of your deployment pipeline or integrate it with NGINX Amplify for continuous monitoring.

    Testing NGINX HSTS Implementation

    Verify Header Presence

    Test that the HSTS header is present on HTTPS responses:

    curl -sI https://example.com | grep -i "strict-transport"
    

    Expected output:

    strict-transport-security: max-age=63072000; includeSubDomains; preload
    

    Verify Header Absence on HTTP

    Confirm the header is NOT present on HTTP redirects:

    curl -sI http://example.com | grep -i "strict-transport"
    

    This should return no output. If it returns a header, your configuration is incorrect—browsers will ignore it anyway, but it indicates a misconfiguration.

    Use Online Testing Tools

    Browser Developer Tools

    In Chrome or Firefox, open Developer Tools (F12), navigate to the Network tab, and inspect response headers. Look for the Strict-Transport-Security header on HTTPS responses.

    Gradual NGINX HSTS Deployment

    For sites without existing HSTS, deploy gradually to avoid locking users out if issues arise.

    Phase 1: Testing (1-2 weeks)

    Start with a one-day max-age:

    add_header Strict-Transport-Security "max-age=86400" always;
    

    Verify all pages load correctly over HTTPS. Check for mixed content warnings. Ensure all internal links use HTTPS or protocol-relative URLs.

    Phase 2: Subdomains (2-4 weeks)

    Extend to one week and add includeSubDomains:

    add_header Strict-Transport-Security "max-age=604800; includeSubDomains" always;
    

    Verify all subdomains have valid SSL certificates and function correctly over HTTPS.

    Phase 3: Production (ongoing)

    Increase to one year minimum:

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    

    Monitor for issues. Users are now locked to HTTPS for one year.

    Phase 4: Preload (permanent)

    Add the preload directive and increase to two years:

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    

    Submit to hstspreload.org. Understand this is essentially permanent—removal is slow and affects all users.

    HSTS works alongside other security headers for comprehensive protection. Consider adding these to your NGINX configuration:

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    
    # Prevent MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;
    
    # Clickjacking protection
    add_header X-Frame-Options "SAMEORIGIN" always;
    
    # Content Security Policy (customize for your site)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
    
    # Referrer Policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    

    For complete TLS hardening, see our NGINX TLS 1.3 hardening guide which covers cipher selection, OCSP stapling, and achieving an A+ SSL Labs rating.

    Conclusion

    NGINX HSTS configuration requires attention to redirect patterns and understanding of the includeSubDomains directive. The key points:

    With proper NGINX HSTS configuration, you eliminate SSL stripping attacks for returning visitors. With preload, you also protect first-time visitors. Combined with strong TLS configuration, NGINX HSTS is an essential component of modern web security.

    The official Mozilla Web Security Guidelines and RFC 6797 provide additional reference material on HSTS implementation.

    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

    Exit mobile version