HTTPS Behind a Load Balancer
Overview
This guide explains how to ensure proper HTTPS handling when running the application behind a load balancer (such as AWS ELB) that terminates SSL.
Problem
When using a load balancer that terminates SSL, the load balancer handles the HTTPS connection and then forwards the request to the container as HTTP. This can cause issues where:
- The application doesn't know that the original request was HTTPS
- When generating URLs or redirects, the application uses HTTP instead of HTTPS
- API requests may be redirected from HTTPS to HTTP, causing security warnings in browsers
- Mixed content warnings may appear when a page loaded over HTTPS tries to load resources over HTTP
The specific symptoms observed were:
* Connection to host tracker.glimpse.technology left intact
* Clear auth, redirects to port from 443 to 80
* Issue another request to this URL: 'http://tracker.glimpse.technology/api/v1/trackers/?limit=2000&skip=0&include_latest_location=true&sort_field=last_report_received&sort_order=desc'
* Host tracker.glimpse.technology:80 was resolved.
And:
Mixed Content: The page at 'https://tracker-admin.glimpse.technology/clients' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://tracker-admin.glimpse.technology/api/v1/clients/?skip=0&limit=10&sort_field=name&sort_order=asc'. This request has been blocked; the content must be served over HTTPS.
Solution
The solution is to ensure that:
- The
X-Forwarded-Protoheader is properly set to "https" for all API requests - The Content-Security-Policy header is configured to allow connections only to HTTPS resources
- The nginx proxy is configured to use the correct API URL (which may be different in development and production)
- The Strict-Transport-Security header is set to enforce HTTPS
- IPv6 connectivity issues are addressed
- 307 redirects are properly handled
Changes Made
- Modified the Nginx configuration to use environment variables for the API URL:
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://$host https://*.glimpse.technology https://api.example.com;";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Global resolver for DNS resolution
resolver 1.1.1.1 ipv6=off;
resolver_timeout 5s;
# Special case for auth login endpoint
location = /api/v1/auth/login/json {
proxy_pass __API_URL__/api/v1/auth/login/json;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host tracker.glimpse.technology;
proxy_cache_bypass $http_upgrade;
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";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
}
# Special case for users/me endpoint
location = /api/v1/users/me {
proxy_pass __API_URL__/api/v1/users/me/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host tracker.glimpse.technology;
proxy_cache_bypass $http_upgrade;
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";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
}
# Handle API requests
location /api/ {
# Proxy to the API server (set via environment variable)
proxy_pass __API_URL__/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
# Set the Host header based on the API URL
proxy_set_header Host tracker.glimpse.technology;
proxy_cache_bypass $http_upgrade;
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";
# SSL configuration for proxy (if using HTTPS)
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
# Follow redirects for trailing slashes
proxy_redirect off;
proxy_intercept_errors on;
error_page 301 302 307 = @handle_redirect;
}
# Handle redirects
location @handle_redirect {
set $saved_redirect_location $upstream_http_location;
proxy_pass $saved_redirect_location;
}
- Updated the Dockerfile to use environment variables for the API URL:
# Create a script to replace environment variables in nginx config
RUN echo '#!/bin/sh' > /docker-entrypoint.sh && \
echo 'API_URL=${API_URL:-http://api:8000}' >> /docker-entrypoint.sh && \
echo 'echo "Using API URL: $API_URL"' >> /docker-entrypoint.sh && \
echo 'sed -i "s|__API_URL__|$API_URL|g" /etc/nginx/conf.d/default.conf' >> /docker-entrypoint.sh && \
echo 'nginx -g "daemon off;"' >> /docker-entrypoint.sh && \
chmod +x /docker-entrypoint.sh
# Set the entrypoint to our script
ENTRYPOINT ["/docker-entrypoint.sh"]
# Override the CMD since we're running nginx directly in the entrypoint
CMD [""]
- Updated the docker-compose files to set the API URL:
admin:
build:
context: ./tracker-admin
dockerfile: Dockerfile
ports:
- 8080:80
environment:
- API_URL=${API_URL:-https://tracker.glimpse.technology} # Default to production URL, can be overridden
depends_on:
- api
networks:
- tracker-network
restart: unless-stopped
For development environments, we use a different default:
admin-dev:
build:
context: ./tracker-admin
dockerfile: build/dev/Dockerfile
ports:
- 3000:3000
volumes:
- ./tracker-admin:/app
env_file:
- ./tracker-admin/.env
environment:
- API_URL=${API_URL:-http://dev:8000} # Default to development URL, can be overridden
depends_on:
- dev
networks:
- tracker-network
restart: unless-stopped
user: root
Why This Works
The updated configuration ensures that:
- The API knows that the original request was HTTPS (via the
X-Forwarded-Protoheader) - When generating URLs or redirects, the API will use HTTPS instead of HTTP
- API requests will maintain their HTTPS protocol, preventing security warnings in browsers
- The Content-Security-Policy header prevents loading resources over HTTP
- The Strict-Transport-Security header tells browsers to always use HTTPS for future requests
- The proxy_pass directive uses HTTPS to connect to the API server
- The Host header is set to the correct API domain
Additional Considerations
- This solution assumes that all production traffic should be using HTTPS
- The load balancer should be configured to forward the original protocol in the
X-Forwarded-Protoheader - For local development, this change should not affect functionality as the API will still work with HTTP
- The Content-Security-Policy header includes
https://$hostwhich allows connections to the same host over HTTPS - The Content-Security-Policy header includes
https://*.glimpse.technologywhich allows connections to all subdomains over HTTPS
Testing
To verify that this solution works:
- Deploy the updated containers to production
- Access the application via HTTPS
- Monitor network requests in the browser's developer tools to ensure all API requests use HTTPS
- Verify that no redirects from HTTPS to HTTP occur
- Check for any mixed content warnings in the browser's console
Troubleshooting
If you encounter mixed content warnings:
- Check the browser's console to see which resources are being loaded over HTTP
- Verify that the Content-Security-Policy header is correctly configured
- Ensure that the nginx proxy is correctly configured to use HTTPS when connecting to the API server
- Clear the browser's cache to ensure it's not using cached HTTP resources
- Check for IPv6 connectivity issues in the nginx logs
- Ensure that the nginx proxy is correctly handling 307 redirects
IPv6 Connectivity Issues
If you see errors like the following in your nginx logs:
connect() to [2606:4700:3030::6815:4907]:443 failed (101: Network unreachable) while connecting to upstream
This indicates that nginx is trying to connect to the API server using IPv6, but the connection is failing. To fix this, add a global resolver directive at the server level in your nginx configuration to disable IPv6:
# Global resolver for DNS resolution
resolver 1.1.1.1 ipv6=off;
resolver_timeout 5s;
Using Cloudflare's public DNS server (1.1.1.1) with IPv6 explicitly disabled can provide more reliable DNS resolution than the default Docker DNS (127.0.0.11). The shorter timeout (5s) helps prevent hanging connections.
It's important to place this directive at the server level, not within a location block, to ensure it applies globally to all DNS resolutions performed by nginx.
Handling 307 Redirects
If you see 307 Temporary Redirect responses in your logs, it means that the API server is redirecting the request to a different URL. This often happens when the URL doesn't have the correct trailing slash. There are two approaches to handling this:
1. Special Case Handling for Problematic Endpoints
The first approach is to handle specific endpoints that are known to cause issues:
# Special case for auth login endpoint
location = /api/v1/auth/login/json {
proxy_pass __API_URL__/api/v1/auth/login/json;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host tracker.glimpse.technology;
proxy_cache_bypass $http_upgrade;
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";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
}
# Special case for users/me endpoint
location = /api/v1/users/me {
proxy_pass __API_URL__/api/v1/users/me/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host tracker.glimpse.technology;
proxy_cache_bypass $http_upgrade;
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";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
}
This approach is more efficient as it prevents the redirect from happening in the first place. Note that some endpoints like /api/v1/users/me need special handling with exact matching using the = operator.
2. Follow Redirects Automatically
The second approach is to intercept and follow redirects automatically:
# Follow redirects for trailing slashes
proxy_redirect off;
proxy_intercept_errors on;
error_page 301 302 307 = @handle_redirect;
# Handle redirects
location @handle_redirect {
set $saved_redirect_location $upstream_http_location;
proxy_pass $saved_redirect_location;
}
This will intercept the redirect response and follow it automatically, without requiring the client to make a second request.
Using both approaches together provides the most robust solution, as it handles both known endpoint patterns and any unexpected redirects.