How to Redirect HTTP to HTTPS Behind AWS ELB Without Using If in Nginx


1 views

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:

  1. Keep the health check endpoint accessible via HTTP
  2. 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