Skip to main content

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:DescribeInstances
  • ec2:DescribeAddresses
  • ec2:DisassociateAddress
  • ec2:ReleaseAddress
  • ec2: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:

  1. AWS Systems Manager Session Manager - Recommended. Provides secure shell access without public IPs or open inbound ports. See Setting up Session Manager.

  2. Bastion host / Jump box - An instance in a public subnet that you connect to first, then hop to private instances.

  3. VPN connection - AWS Client VPN or Site-to-Site VPN provides private network access.

  4. 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)

  1. Go to EC2 Console in us-east-1
  2. Click Instances in the left sidebar
  3. Select the instance with the public IP
  4. Click the Networking tab
  5. Look for Elastic IP addresses - if present, note the Elastic IP
  6. Click Elastic IPs in the left sidebar under Network & Security
  7. Select the Elastic IP address
  8. Click Actions > Disassociate Elastic IP address
  9. Click Disassociate
  10. (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:

  1. Go to EC2 Console in us-east-1
  2. Click Instances in the left sidebar
  3. Select the instance with the public IP
  4. Click Actions > Image and templates > Create image (to preserve the current state)
  5. Wait for the AMI to become available
  6. Click Launch instances
  7. Select your new AMI as the source
  8. In Network settings, ensure Auto-assign public IP is set to Disable
  9. Place the instance in a private subnet (a subnet without a route to an Internet Gateway)
  10. Complete the launch wizard
  11. Migrate any Elastic IPs, security groups, or IAM roles as needed
  12. Verify the new instance works correctly
  13. 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:

  1. Confirm no public IP:

    • Go to EC2 Console > Instances
    • Select the instance
    • Check the Networking tab
    • Verify Public IPv4 address shows - or is empty
  2. 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

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.