How to Fix Mixed Content Warnings in Nginx Reverse Proxy with HTTPS Upstreams


2 views

When setting up nginx as a reverse proxy with HTTPS termination, you're encountering mixed content warnings because your backend services are serving resources with hardcoded HTTP URLs. This common architecture pattern - where nginx handles SSL termination while communicating with backend servers over HTTP - requires special configuration to avoid these warnings.

The mixed content warnings occur because:

  1. Your backend services generate HTML with absolute HTTP URLs (e.g., http://domain.com/style.css)
  2. Browsers receive these requests through HTTPS but see insecure HTTP resources
  3. Modern browsers block these mixed content requests for security reasons

Here's an enhanced version of your configuration that solves multiple aspects of the problem:

server {
    listen 443 ssl;
    server_name domain.com;

    # SSL configuration
    ssl_certificate /path/to/your/certificate.crt;
    ssl_certificate_key /path/to/your/private.key;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;

    # Service1 configuration with content rewriting
    location /service1/ {
        proxy_pass http://192.168.1.101/;
        
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        
        # Standard proxy headers
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        
        # Content rewriting to fix URLs
        proxy_set_header Accept-Encoding "";
        sub_filter_once off;
        sub_filter_types *;
        sub_filter 'http://domain.com' 'https://domain.com';
        sub_filter 'http://$host' 'https://$host';
    }

    # Similar configuration for service2
    location /service2/ {
        proxy_pass http://192.168.1.102/;
        # ... same configuration as above ...
    }
}

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

1. HTTP to HTTPS Redirect

The separate server block ensures all HTTP traffic gets redirected to HTTPS, preventing any accidental HTTP access.

2. Content Rewriting with sub_filter

The sub_filter directives actively rewrite HTTP URLs to HTTPS in the response bodies. This handles cases where backends hardcode URLs.

3. Proper Header Forwarding

X-Forwarded-Proto tells backends the original request was HTTPS, allowing them to generate correct URLs if they're protocol-aware.

4. Security Headers

Additional headers like HSTS enforce HTTPS usage and improve overall security.

For more complex scenarios, consider these additions:

# For WebSocket support
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

# Custom error pages
proxy_intercept_errors on;
error_page 502 /custom_502.html;
location = /custom_502.html {
    root /usr/share/nginx/html;
    internal;
}

# Buffering and timeouts
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 24k;

After implementing these changes:

  1. Test with browser developer tools (Network tab) to verify all resources load via HTTPS
  2. Check for any remaining mixed content warnings in console
  3. Verify redirects work properly using curl: curl -I http://domain.com
  4. Test backend connectivity: curl -H "Host: domain.com" http://localhost/service1/

If content rewriting isn't sufficient:

1. Header Injection

Some frameworks respect the X-Forwarded-Proto header if properly configured:

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;

2. Application-Level Fixes

For maximum compatibility, consider updating applications to:

  • Use protocol-relative URLs (//domain.com/resource)
  • Read the X-Forwarded-Proto header
  • Support configuration of base URLs

When implementing Nginx as a reverse proxy with HTTPS termination, many developers encounter mixed content warnings when the backend servers return HTTP-absolute URLs. The browser detects this discrepancy between the secure HTTPS page and insecure HTTP resources, blocking them for security reasons.


# Typical error message in browser console:
Mixed Content: The page at 'https://domain.com/service1' was loaded over HTTPS, 
but requested an insecure resource 'http://domain.com/service1/style.css'

Here's a comprehensive configuration that solves multiple aspects of the mixed content problem:


server {
    listen 443 ssl;
    server_name domain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";

    location /service1/ {
        proxy_pass http://192.168.1.101/;
        
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        
        # Critical headers for mixed content resolution
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        
        # Content rewriting
        proxy_set_header Accept-Encoding "";
        sub_filter_types *;
        sub_filter 'http://domain.com' 'https://domain.com';
        sub_filter_once off;
    }
    
    location /service2/ {
        proxy_pass http://192.168.1.102/;
        # Similar configuration as above
    }
}

1. Protocol Headers: The X-Forwarded-Proto header informs backend servers about the original request scheme.

2. Content Rewriting: The sub_filter directive dynamically modifies response content, converting HTTP URLs to HTTPS.

3. WebSocket Support: The Upgrade headers ensure WebSocket connections work through the proxy.

For applications that use JavaScript to construct URLs:


# In your Nginx config
location / {
    # ... other proxy settings ...
    
    # Set a cookie that JavaScript can read
    add_header Set-Cookie "frontend_https=1; Path=/";
    
    # Or inject a meta tag
    sub_filter '</head>' '<meta name="frontend_https" content="true"></head>';
}

Then in your JavaScript:


const isHTTPS = document.cookie.includes('frontend_https=1') || 
                document.querySelector('meta[name="frontend_https"]') !== null;
                
const API_BASE = isHTTPS ? 'https://domain.com' : 'http://domain.com';

Verify your setup with these commands:


# Check Nginx configuration syntax
sudo nginx -t

# Check headers
curl -I https://domain.com/service1/

# Test content rewriting
curl https://domain.com/service1/ | grep 'https://domain.com'