EC2 Instance Public IP Address
Overview
This check identifies EC2 instances that have a public IPv4 address assigned. Public IPs make instances directly accessible from the internet, which is often unnecessary and increases your attack surface.
Most workloads should run in private subnets without public IPs. Instead, use load balancers, NAT gateways, or AWS Systems Manager Session Manager to provide controlled access.
Risk
EC2 instances with public IP addresses are exposed to several threats:
- Direct internet exposure: Attackers can scan and probe your instance directly from anywhere on the internet
- Brute force attacks: Management ports (SSH, RDP) become targets for automated credential-guessing attacks
- Vulnerability exploitation: Any unpatched service running on the instance can be attacked directly
- DDoS attacks: Public instances can receive direct denial-of-service traffic
- Data exfiltration: Compromised instances with public IPs can more easily send data to external destinations
Removing unnecessary public IPs significantly reduces your exposure to these threats.
Remediation Steps
Prerequisites
You need:
- AWS Console access with permissions to manage EC2 instances and Elastic IPs
- Understanding of how the instance is currently accessed (you will need an alternative access method)
Required IAM permissions (for administrators)
Your IAM user or role needs these permissions:
ec2:DescribeInstancesec2:DescribeAddressesec2:DisassociateAddressec2:ReleaseAddressec2:RunInstances(if launching a replacement instance)ec2:TerminateInstances(if terminating the old instance)
Before you begin: Plan alternative access
Before removing a public IP, ensure you have another way to access the instance:
-
AWS Systems Manager Session Manager - Recommended. Provides secure shell access without public IPs or open inbound ports. See Setting up Session Manager.
-
Bastion host / Jump box - An instance in a public subnet that you connect to first, then hop to private instances.
-
VPN connection - AWS Client VPN or Site-to-Site VPN provides private network access.
-
AWS EC2 Instance Connect Endpoint - Allows SSH connections to instances in private subnets without public IPs.
AWS Console Method
There are two scenarios depending on how the public IP was assigned:
Scenario A: Elastic IP (manually assigned)
- Go to EC2 Console in us-east-1
- Click Instances in the left sidebar
- Select the instance with the public IP
- Click the Networking tab
- Look for Elastic IP addresses - if present, note the Elastic IP
- Click Elastic IPs in the left sidebar under Network & Security
- Select the Elastic IP address
- Click Actions > Disassociate Elastic IP address
- Click Disassociate
- (Optional) If you no longer need the Elastic IP, select it again and click Actions > Release Elastic IP address to avoid charges
Scenario B: Auto-assigned public IP
Auto-assigned public IPs cannot be removed from a running instance. You must launch a new instance without a public IP and migrate your workload:
- Go to EC2 Console in us-east-1
- Click Instances in the left sidebar
- Select the instance with the public IP
- Click Actions > Image and templates > Create image (to preserve the current state)
- Wait for the AMI to become available
- Click Launch instances
- Select your new AMI as the source
- In Network settings, ensure Auto-assign public IP is set to Disable
- Place the instance in a private subnet (a subnet without a route to an Internet Gateway)
- Complete the launch wizard
- Migrate any Elastic IPs, security groups, or IAM roles as needed
- Verify the new instance works correctly
- Terminate the old instance
AWS CLI (optional)
Find instances with public IPs
aws ec2 describe-instances \
--query "Reservations[*].Instances[?PublicIpAddress!=null].[InstanceId,PublicIpAddress,Tags[?Key=='Name'].Value|[0]]" \
--output table \
--region us-east-1
Check if an IP is an Elastic IP
aws ec2 describe-addresses \
--filters "Name=public-ip,Values=<public-ip-address>" \
--region us-east-1
If this returns results, the IP is an Elastic IP and can be disassociated.
Disassociate an Elastic IP
First, get the association ID:
aws ec2 describe-addresses \
--filters "Name=public-ip,Values=<public-ip-address>" \
--query "Addresses[0].AssociationId" \
--output text \
--region us-east-1
Then disassociate:
aws ec2 disassociate-address \
--association-id <association-id> \
--region us-east-1
Release an Elastic IP (optional)
If you no longer need the Elastic IP:
aws ec2 describe-addresses \
--filters "Name=public-ip,Values=<public-ip-address>" \
--query "Addresses[0].AllocationId" \
--output text \
--region us-east-1
aws ec2 release-address \
--allocation-id <allocation-id> \
--region us-east-1
CloudFormation (optional)
When defining EC2 instances in CloudFormation, set AssociatePublicIpAddress to false in the network interface configuration:
AWSTemplateFormatVersion: '2010-09-09'
Description: EC2 instance without public IP address
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC to launch the instance in
PrivateSubnetId:
Type: AWS::EC2::Subnet::Id
Description: Private subnet for the instance (no IGW route)
InstanceType:
Type: String
Default: t3.micro
Description: EC2 instance type
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Description: Latest Amazon Linux 2023 AMI
Resources:
InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for private EC2 instance
VpcId: !Ref VpcId
SecurityGroupIngress: []
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS outbound for SSM
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref InstanceRole
PrivateInstance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
ImageId: !Ref LatestAmiId
IamInstanceProfile: !Ref InstanceProfile
NetworkInterfaces:
- DeviceIndex: '0'
SubnetId: !Ref PrivateSubnetId
AssociatePublicIpAddress: false
GroupSet:
- !Ref InstanceSecurityGroup
Tags:
- Key: Name
Value: PrivateInstance
Outputs:
InstanceId:
Description: Instance ID
Value: !Ref PrivateInstance
PrivateIp:
Description: Private IP address
Value: !GetAtt PrivateInstance.PrivateIp
Deploy with:
aws cloudformation deploy \
--template-file private-instance.yaml \
--stack-name private-ec2-instance \
--parameter-overrides \
VpcId=vpc-xxxxxxxx \
PrivateSubnetId=subnet-xxxxxxxx \
--capabilities CAPABILITY_IAM \
--region us-east-1
Terraform (optional)
variable "vpc_id" {
description = "VPC ID to launch the instance in"
type = string
}
variable "private_subnet_id" {
description = "Private subnet ID (no IGW route)"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
# Get latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Security group for private instance
resource "aws_security_group" "private_instance" {
name = "private-instance-sg"
description = "Security group for private EC2 instance"
vpc_id = var.vpc_id
# HTTPS outbound for SSM
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS outbound for SSM"
}
tags = {
Name = "private-instance-sg"
}
}
# IAM role for SSM access
resource "aws_iam_role" "instance_role" {
name = "private-instance-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ssm_policy" {
role = aws_iam_role.instance_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "instance_profile" {
name = "private-instance-profile"
role = aws_iam_role.instance_role.name
}
# EC2 instance without public IP
resource "aws_instance" "private_instance" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = var.instance_type
subnet_id = var.private_subnet_id
vpc_security_group_ids = [aws_security_group.private_instance.id]
iam_instance_profile = aws_iam_instance_profile.instance_profile.name
associate_public_ip_address = false
tags = {
Name = "PrivateInstance"
}
}
output "instance_id" {
description = "Instance ID"
value = aws_instance.private_instance.id
}
output "private_ip" {
description = "Private IP address"
value = aws_instance.private_instance.private_ip
}
Deploy with:
terraform init
terraform plan -var="vpc_id=vpc-xxxxxxxx" -var="private_subnet_id=subnet-xxxxxxxx"
terraform apply -var="vpc_id=vpc-xxxxxxxx" -var="private_subnet_id=subnet-xxxxxxxx"
Verification
After removing the public IP:
-
Confirm no public IP:
- Go to EC2 Console > Instances
- Select the instance
- Check the Networking tab
- Verify Public IPv4 address shows
-or is empty
-
Test alternative access:
- If using Session Manager: Go to Systems Manager > Session Manager > Start session
- Select your instance and click Start session
- Verify you can connect
CLI verification commands
Check that an instance has no public IP:
aws ec2 describe-instances \
--instance-ids <instance-id> \
--query "Reservations[0].Instances[0].PublicIpAddress" \
--output text \
--region us-east-1
Expected output: None or empty.
List all instances still with public IPs:
aws ec2 describe-instances \
--query "Reservations[*].Instances[?PublicIpAddress!=null].[InstanceId,PublicIpAddress]" \
--output table \
--region us-east-1
Additional Resources
- AWS Documentation: IP Addressing for EC2 Instances
- AWS Documentation: Elastic IP Addresses
- AWS Documentation: Session Manager
- AWS Blog: New - Use AWS PrivateLink to Access AWS Lambda Over Private AWS Network
- AWS Well-Architected: Infrastructure Protection
Notes
-
Consider cost implications: Removing an Elastic IP without releasing it still incurs charges. If you do not need the IP, release it. However, once released, you cannot get the same IP back.
-
Service interruption: Removing a public IP will immediately disconnect any sessions using that IP. Plan the change during a maintenance window if the instance serves users directly.
-
Load balancer alternative: For web applications, place instances behind an Application Load Balancer (ALB) in private subnets. The ALB handles public traffic while your instances remain private.
-
NAT Gateway for outbound traffic: Instances in private subnets need a NAT Gateway or NAT instance to reach the internet for updates and patches.
-
VPC Endpoints: Use VPC endpoints for AWS services (S3, DynamoDB, etc.) to avoid routing traffic through the internet entirely.
-
Auto-assigned vs. Elastic IP: Auto-assigned public IPs change when an instance is stopped and started. Elastic IPs remain the same until explicitly released. Both represent security risk if unnecessary.
-
Subnet configuration: The best practice is to configure subnets without automatic public IP assignment. This prevents new instances from accidentally getting public IPs.