How to Prevent Nginx Reverse Proxy from Forcing HTTPS to HTTP Redirects While Preserving HTTP Traffic


2 views

When configuring Nginx as a reverse proxy for applications that need to serve both HTTP and HTTPS traffic, a common pain point emerges: upstream redirects from HTTPS getting downgraded to HTTP. This typically occurs when the backend application (like Gunicorn in your case) issues HTTP 3xx redirect responses.

# Problematic scenario:
1. User accesses https://domain.com/login
2. Application returns redirect to http://domain.com/dashboard
3. Security broken with protocol downgrade

The naive solution of globally rewriting redirects with proxy_redirect http:// https:// creates the opposite problem - it forces HTTPS even for originally HTTP requests. This violates the principle of protocol neutrality and can break certain use cases.

Here are two maintainable approaches that don't require separate server blocks or external includes:

1. X-Forwarded-Proto Aware Solution

Modern web frameworks respect the X-Forwarded-Proto header. Combine this with conditional proxy_redirect:

map $http_x_forwarded_proto $redirect_scheme {
    default $scheme;
    "https" "https";
}

server {
    # ... existing config ...
    proxy_redirect http:// $redirect_scheme://;
    proxy_set_header X-Forwarded-Proto $scheme;
}

2. Protocol-Sensitive Redirect Rewriting

For more complex scenarios, use Nginx's more_set_headers from the Headers More module:

location @proxy {
    # ... existing proxy settings ...
    proxy_redirect ~^http://([^/]+)(/.+)$ $scheme://$1$2;
}

Consider these additions to make your configuration bulletproof:

# Prevent protocol confusion in absolute redirects
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

# Handle WebSocket upgrades properly
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Verify your solution works under various scenarios:

# Test commands
curl -I http://domain.com/login
curl -Ik https://domain.com/login
curl -v http://domain.com/api/redirect
curl -vk https://domain.com/api/redirect

When configuring Nginx as a reverse proxy for both HTTP and HTTPS traffic, a common pain point emerges: upstream applications issuing redirects often force HTTPS requests back to HTTP. This occurs because the backend application (like Gunicorn in your case) typically isn't protocol-aware when generating redirect responses.

Your existing setup handles both protocols in a single server block:

server {
    listen 80;
    listen 443 ssl;
    ...
    proxy_set_header X-Forwarded-Proto https;
    proxy_pass http://gunicorn;
}

The critical issue stems from how Nginx processes the backend's Location headers. When the backend sends a redirect with Location: http://domain.com/path, Nginx passes it through verbatim unless explicitly instructed otherwise.

The brute-force solution of:

proxy_redirect http:// https://;

works for HTTPS traffic but creates two problems:

  1. It forces ALL redirects to HTTPS, even for HTTP-originating requests
  2. It doesn't handle port numbers in redirects (common in development environments)

The proper approach uses Nginx's built-in variables to make protocol-aware redirects:

server {
    listen 80;
    listen 443 ssl;
    ...
    
    location @proxy {
        ...
        proxy_redirect http://$host https://$host;
        proxy_redirect http://$http_host https://$http_host;
    }
}

For more complex environments with:

  • Multiple domains
  • Non-standard ports
  • Load balancers

Consider this enhanced version:

map $http_x_forwarded_proto $redirect_scheme {
    default $scheme;
    "https" "https";
}

server {
    ...
    proxy_redirect http:// $redirect_scheme://;
    proxy_set_header X-Forwarded-Proto $scheme;
}

For enterprise deployments, we recommend:

server {
    listen 80;
    server_name domain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name domain.com;
    
    location / {
        proxy_pass http://backend;
        proxy_redirect http:// https://;
        proxy_cookie_path / "/; secure; SameSite=strict";
    }
}

This approach completely separates HTTP and HTTPS handling while ensuring all traffic ultimately uses HTTPS.