When using Amazon Elastic Load Balancer (ELB) to handle HTTPS traffic, a common challenge arises: the ELB terminates SSL/TLS and forwards requests to your backend servers as plain HTTP. This makes it tricky to implement proper HTTP-to-HTTPS redirection in Nginx without using the discouraged if
directive.
The standard Nginx redirection configuration:
server {
listen 80;
server_name www.example.org;
return 301 https://$server_name$request_uri;
}
won't work because all requests - both originally HTTP and HTTPS - arrive at your Nginx server on port 80. The ELB strips the HTTPS information during forwarding.
Amazon ELB adds an X-Forwarded-Proto
header that indicates the original protocol. We can leverage this header for redirection:
server {
listen 80;
server_name www.example.org;
if ($http_x_forwarded_proto != 'https') {
return 301 https://$server_name$request_uri;
}
# Rest of your configuration
...
}
While this works, it uses the if
directive which is generally discouraged in Nginx configurations due to potential performance implications.
Here's a better approach that avoids if
entirely by using two server blocks:
# Redirect HTTP to HTTPS
server {
listen 80;
server_name www.example.org;
return 301 https://$server_name$request_uri;
}
# Main server block for HTTPS traffic
server {
listen 80;
server_name www.example.org;
# Only process requests that came through HTTPS
set $redirect_to_https 0;
if ($http_x_forwarded_proto != 'https') {
set $redirect_to_https 1;
}
if ($request_uri != '/health-check') {
set $redirect_to_https "${redirect_to_https}1";
}
if ($redirect_to_https = '11') {
return 301 https://$server_name$request_uri;
}
# Health check endpoint
location = /health-check {
access_log off;
return 200 'OK';
}
# Rest of your configuration
...
}
The solution above includes a special case for ELB health checks. The ELB needs to reach your server via HTTP for health monitoring, so we need to:
- Keep the health check endpoint accessible via HTTP
- Still redirect all other HTTP traffic to HTTPS
While the solution using if
might seem less optimal, in practice:
- The performance impact is negligible for most applications
- The configuration is simpler and more maintainable
- It properly handles all edge cases including health checks
If you're still concerned about Nginx performance, you could implement the redirection in your application (e.g., Django middleware):
class SSLRedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.META.get('HTTP_X_FORWARDED_PROTO', 'http') != 'https':
from django.http import HttpResponsePermanentRedirect
return HttpResponsePermanentRedirect(
'https://' + request.get_host() + request.path
)
return self.get_response(request)
However, this approach is generally less efficient than handling it at the web server level.
For most use cases, the Nginx solution using X-Forwarded-Proto
is the best approach, despite using if
. The benefits outweigh the minimal performance impact, especially when compared to the complexity of alternative solutions.
When operating behind an AWS Elastic Load Balancer (ELB), a common requirement is redirecting HTTP traffic to HTTPS while maintaining health check functionality. The configuration becomes tricky because ELB terminates SSL and forwards requests as HTTP to backend instances.
The typical Nginx redirect approach:
server {
listen 80;
server_name www.example.org;
return 301 https://$server_name$request_uri;
}
fails because all requests (both original HTTP and HTTPS) arrive at Nginx as HTTP traffic on port 80.
AWS ELB includes the X-Forwarded-Proto
header which indicates the original protocol. We can leverage this without using if
:
server {
listen 80;
server_name www.example.org;
# Match requests that came through HTTP
set $redirect_to_https 0;
if ($http_x_forwarded_proto = 'http') {
set $redirect_to_https 1;
}
# Allow ELB health checks to pass
location /health-check {
access_log off;
return 200;
}
# Perform the redirect
if ($redirect_to_https = 1) {
return 301 https://$server_name$request_uri;
}
# Your normal proxy pass or root location
location / {
proxy_pass http://backend;
# Other proxy settings...
}
}
For those who want to completely avoid if
blocks, here's a map
-based solution:
map $http_x_forwarded_proto $should_redirect {
default 0;
http 1;
}
server {
listen 80;
server_name www.example.org;
location /health-check {
access_log off;
return 200;
}
if ($should_redirect) {
return 301 https://$server_name$request_uri;
}
# Your normal proxy configuration
location / {
proxy_pass http://backend;
}
}
While the if
directive has performance implications in some contexts, in this particular case:
- The overhead is negligible compared to the SSL handshake that's being avoided
- The ELB already provides health check caching
- The map-based solution adds minimal processing overhead
Ensure your ELB security group only allows HTTPS traffic from the internet, while maintaining HTTP between ELB and instances. This prevents direct HTTP access bypassing the redirect.
Verify your setup with these commands:
# Test HTTP redirect
curl -I -H "Host: www.example.org" http://your-elb-address
# Test HTTPS access (should show 200)
curl -I -k https://www.example.org
# Test health check endpoint
curl -I http://your-instance-ip/health-check