Conditional HSTS Header in Nginx Based on Upstream Response


1 views

When working with reverse proxy configurations in Nginx, we often need to conditionally modify headers based on upstream server responses. A common use case involves HTTP Strict Transport Security (HSTS) headers, where we want to:

1. Default to adding HSTS headers
2. Allow upstream servers to override this behavior
3. Avoid header duplication

The naive approach using if doesn't work because:

  • Nginx processes if directives during the rewrite phase
  • Upstream headers aren't available until later processing stages
  • add_header directives in if blocks often get ignored

The most reliable approach uses Nginx's map to create conditional logic:

http {
    map $upstream_http_strict_transport_security $hsts_header {
        default   "max-age=15552000";
        ""        "max-age=15552000";
        "~*"      $upstream_http_strict_transport_security;
    }

    server {
        location / {
            proxy_pass http://webservers;
            add_header Strict-Transport-Security $hsts_header;
            
            # Other proxy settings
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $remote_addr;
        }
    }
}

For more complex scenarios, Lua scripting offers greater flexibility:

location / {
    proxy_pass http://webservers;
    
    header_filter_by_lua_block {
        if not ngx.header["Strict-Transport-Security"] then
            ngx.header["Strict-Transport-Security"] = "max-age=15552000"
        end
    }
}

Verify your configuration with:

curl -I https://yourdomain.com

Key test cases:

  1. Upstream with no HSTS header → Should add default
  2. Upstream with HSTS header → Should preserve original
  3. Upstream with max-age=0 → Should pass through unchanged

The map solution has minimal overhead as it:

  • Executes during configuration load
  • Uses simple pattern matching
  • Doesn't require additional modules

When working with Nginx reverse proxy configurations, there are cases where you need to conditionally add response headers based on whether the upstream server has already set them. A common use case involves HTTP Strict Transport Security (HSTS) headers where:

  • Most sites should have HSTS enabled by default
  • Specific upstream servers might need to opt-out
  • The proxy should respect upstream's header decisions

The naive solution using if blocks doesn't work because:

if ($upstream_http_strict_transport_security = "") {
    add_header Strict-Transport-Security "max-age=15552000";
}

Nginx's header processing happens before the if evaluation due to its internal processing phases. The add_header directive inside if gets ignored.

Method 1: Using map Directive

This is the most elegant solution that avoids if entirely:

map $upstream_http_strict_transport_security $hsts_header {
    ""      "max-age=15552000";
    default $upstream_http_strict_transport_security;
}

server {
    location / {
        proxy_pass http://webservers;
        # ... other proxy settings ...
        add_header Strict-Transport-Security $hsts_header;
    }
}

Method 2: Using a Variable with Default Value

For simpler cases where you just need a default:

set $hsts_header $upstream_http_strict_transport_security;
if ($hsts_header = "") {
    set $hsts_header "max-age=15552000";
}

location / {
    proxy_pass http://webservers;
    add_header Strict-Transport-Security $hsts_header;
}

When implementing conditional headers:

  • Test with curl -I to verify header behavior
  • Remember that add_header directives inherit only if not present in the current level
  • Consider using always parameter if you need headers in error responses

The same pattern works for other headers like CORS:

map $upstream_http_access_control_allow_origin $cors_header {
    ""      $http_origin;
    default $upstream_http_access_control_allow_origin;
}

location /api/ {
    proxy_pass http://backend;
    add_header Access-Control-Allow-Origin $cors_header;
    add_header Access-Control-Allow-Credentials "true";
}