Skip to content

How to Preserve Original Client IP Addresses When Using Application Load Balancer with EC2 Instances?

7 minute read
Content level: Intermediate
0

This article demonstrates a simple proof of concept to retrieve the original client IP address when using Application Load Balancer with EC2 instances.

The Challenge

Exposing EC2 instances directly to the public internet is dangerous because it increases the attack surface, exposes instances to direct threats like DDoS attacks, brute force attempts, and vulnerability exploits, and makes it difficult to implement centralized security controls. It's recommended to put an Application Load Balancer (ALB) in front of EC2 instances for better security and traffic management. However, when requests pass through the ALB, the source IP address that your EC2 instance sees changes to the ALB's internal IP address instead of the original client IP. Therefore, we will show you how to utilize the HTTP headers containing client information to preserve the original client IP address.

Architecture Solution

IP Preservation Mechanism

This is how it works: When a client request passes through the Application Load Balancer, the ALB automatically adds HTTP headers containing the original client information:

  • X-Forwarded-For: Original client IP address
  • X-Forwarded-Proto: Original protocol (HTTP/HTTPS)
  • X-Forwarded-Port: Original port number

Your application can then extract the original client IP from these headers instead of relying on the direct connection IP (which would be the ALB's internal address).

Architecture Diagram

This is how we prove it works through our infrastructure setup:

ALB-Preserve-IP

PoC Architecture Diagram

Traffic Flow:

  1. Client Request: Internet → ALB (Port 80)
  2. Load Balancer: ALB → EC2 (Port 80) with X-Forwarded-For header
  3. Application: EC2 Python app extracts original client IP from headers

Implementation Details

Application Code

The key implementation focuses on extracting the original client IP from HTTP headers. The EC2 instance runs a Python HTTP server that demonstrates IP preservation:

class IPHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        # Extract original client IP from X-Forwarded-For header
        forwarded_for = self.headers.get('X-Forwarded-For', '')
        original_ip = forwarded_for.split(',')[0].strip() if forwarded_for else 'Not available'
        direct_ip = self.client_address[0]  # This shows ALB's internal IP

This code extracts the original client IP from the X-Forwarded-For header while also showing the direct connection IP (ALB's internal address).

Infrastructure as Code

The complete CloudFormation template defining VPC, subnets, security groups, ALB, and EC2 instance is provided in the Appendix: CloudFormation Template section at the end of this article.

Deployment Process

CloudFormation Deployment

  1. Save the Template: Copy the CloudFormation template from the Appendix section and save it as infrastructure.yaml

  2. Deploy via AWS CLI:

    aws cloudformation create-stack \
      --stack-name PublicIP-PoC \
      --template-body file://infrastructure.yaml \
      --capabilities CAPABILITY_IAM \
      --region us-east-1
  3. Deploy via AWS Console:

    • Navigate to CloudFormation in AWS Console
    • Click "Create stack" → "With new resources"
    • Upload the infrastructure.yaml file
    • Follow the wizard to complete deployment
  4. Get Test URL:

    aws cloudformation describe-stacks \
      --stack-name PublicIP-PoC \
      --query 'Stacks[0].Outputs[?OutputKey==`TestURL`].OutputValue' \
      --output text

Testing and Validation

After deployment, get the ALB address from the CloudFormation stack outputs and access the application.

Step 1: Check Your Public IP check my ip Check My IP - Using curl or web service to verify your current public IP address

Step 2: Verify IP Preservation in Application

verify my ip received by app in EC2 Verify My IP from app within EC2 - The PoC application showing your original IP preserved via X-Forwarded-For header

Validation Result: Both IP addresses match, confirming that the ALB successfully preserves the original client IP through the X-Forwarded-For header while maintaining security by keeping EC2 instances in private subnets.

Security Benefits

Network Isolation: EC2 instances in private subnet, not directly accessible from internet Access Control: Single point of entry through ALB with restrictive security groups Defense in Depth: Multiple security layers (subnets + security groups)

Conclusion

This solution demonstrates how to preserve original client IP addresses when using Application Load Balancer with EC2 instances. The key is extracting the client IP from the X-Forwarded-For header that ALB automatically adds to requests.

Key achievements:

  • IP Preservation: Successfully retrieves original client IP via X-Forwarded-For header
  • Security: EC2 instances protected in private subnet with ALB as single entry point
  • Simplicity: Minimal code required for IP extraction

Appendix: CloudFormation Template (US-EAST-1)

AWSTemplateFormatVersion: '2010-09-09'
Description: 'PublicIP PoC - ALB to EC2 with original IP preservation'

Parameters:
  InstanceType:
    Type: String
    Default: t3.micro
    Description: EC2 instance type

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: PublicIP-PoC-VPC
        - Key: project
          Value: poc-ip

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: PublicIP-PoC-IGW
        - Key: project
          Value: poc-ip

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicIP-PoC-Public-Subnet
        - Key: project
          Value: poc-ip

  # Second Public Subnet (ALB requires 2 AZs)
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicIP-PoC-Public-Subnet-2
        - Key: project
          Value: poc-ip

  # Private Subnet
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.3.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: PublicIP-PoC-Private-Subnet
        - Key: project
          Value: poc-ip

  # Route Tables
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PublicIP-PoC-Public-RT
        - Key: project
          Value: poc-ip

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PublicIP-PoC-Private-RT
        - Key: project
          Value: poc-ip

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  # Security Groups
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Application Load Balancer
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: Allow HTTP from internet
      Tags:
        - Key: Name
          Value: PublicIP-PoC-ALB-SG
        - Key: project
          Value: poc-ip

  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for EC2 instance
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ALBSecurityGroup
          Description: Allow HTTP from ALB only
      Tags:
        - Key: Name
          Value: PublicIP-PoC-EC2-SG
        - Key: project
          Value: poc-ip

  # EC2 Instance
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0c02fb55956c7d316  # Amazon Linux 2023 (us-east-1)
      InstanceType: !Ref InstanceType
      SubnetId: !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          cat > /home/ec2-user/server.py << 'EOF'
          #!/usr/bin/env python3
          import http.server
          import socketserver
          
          class IPHandler(http.server.SimpleHTTPRequestHandler):
              def do_GET(self):
                  forwarded_for = self.headers.get('X-Forwarded-For', '')
                  original_ip = forwarded_for.split(',')[0].strip() if forwarded_for else 'Not available'
                  direct_ip = self.client_address[0]
                  
                  html = f'''<!DOCTYPE html>
          <html>
          <head>
              <title>PublicIP PoC - Original IP Test</title>
          </head>
          <body>
              <h1>PublicIP PoC - Client IP Detection</h1>
              <h2>Original Client IP: <span style="color: green;">{original_ip}</span></h2>
              <h3>Direct Connection IP (ALB): {direct_ip}</h3>
              
              <h3>HTTP Headers:</h3>
              <table border="1">
                  <tr><th>Header</th><th>Value</th></tr>
                  <tr><td>X-Forwarded-For</td><td>{self.headers.get('X-Forwarded-For', 'Not set')}</td></tr>
                  <tr><td>X-Forwarded-Proto</td><td>{self.headers.get('X-Forwarded-Proto', 'Not set')}</td></tr>
                  <tr><td>X-Forwarded-Port</td><td>{self.headers.get('X-Forwarded-Port', 'Not set')}</td></tr>
              </table>
          </body>
          </html>'''
                  
                  self.send_response(200)
                  self.send_header('Content-type', 'text/html')
                  self.end_headers()
                  self.wfile.write(html.encode('utf-8'))
          
          PORT = 80
          with socketserver.TCPServer(("", PORT), IPHandler) as httpd:
              httpd.serve_forever()
          EOF
          
          cd /home/ec2-user
          python3 server.py &
      Tags:
        - Key: Name
          Value: PublicIP-PoC-EC2
        - Key: project
          Value: poc-ip

  # Application Load Balancer
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: PublicIP-PoC-ALB
      Scheme: internet-facing
      Type: application
      Subnets:
        - !Ref PublicSubnet
        - !Ref PublicSubnet2
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Tags:
        - Key: Name
          Value: PublicIP-PoC-ALB
        - Key: project
          Value: poc-ip

  # Target Group
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: PublicIP-PoC-TG
      Port: 80
      Protocol: HTTP
      VpcId: !Ref VPC
      TargetType: instance
      Targets:
        - Id: !Ref EC2Instance
          Port: 80
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      Tags:
        - Key: Name
          Value: PublicIP-PoC-TG
        - Key: project
          Value: poc-ip

  # Listener
  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      Port: 80
      Protocol: HTTP

Outputs:
  ALBEndpoint:
    Description: Application Load Balancer DNS name
    Value: !GetAtt ApplicationLoadBalancer.DNSName
    Export:
      Name: !Sub "${AWS::StackName}-ALB-DNS"
  
  TestURL:
    Description: URL to test the PoC
    Value: !Sub "http://${ApplicationLoadBalancer.DNSName}"
  
  VPCId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}-VPC-ID"

IMPORTANT: Please remember to DELETE YOUR STACK AFTER POC to avoid ongoing costs!