How to Preserve Original Client IP Addresses When Using Application Load Balancer with EC2 Instances?
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:
PoC Architecture Diagram
Traffic Flow:
- Client Request: Internet → ALB (Port 80)
- Load Balancer: ALB → EC2 (Port 80) with X-Forwarded-For header
- 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
-
Save the Template: Copy the CloudFormation template from the Appendix section and save it as
infrastructure.yaml
-
Deploy via AWS CLI:
aws cloudformation create-stack \ --stack-name PublicIP-PoC \ --template-body file://infrastructure.yaml \ --capabilities CAPABILITY_IAM \ --region us-east-1
-
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
-
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 - Using curl or web service to verify your current public IP address
Step 2: Verify IP Preservation in Application
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!
- Language
- English