EC2 instances should not have a public IP address
An EC2 instance with a public IP is directly reachable from the internet. Even with restrictive security groups, any misconfiguration or future rule change exposes the instance to the full internet attack surface. Vulnerabilities in the OS, application, or metadata service (like SSRF to IMDSv1) become remotely exploitable.
Keeping instances in private subnets and routing inbound traffic through ALBs, NLBs, or API Gateway limits the blast radius of a compromise and centralizes TLS termination, WAF inspection, and access logging.
Retrofit consideration
Removing a public IP from a running instance means stopping and relaunching it in a private subnet, which causes downtime. You also need to provision NAT gateways, update DNS records, and rearchitect any service that relied on direct public access. Direct SSH/RDP must be replaced with load balancers plus bastion hosts or SSM Session Manager.
Implementation
Choose the approach that matches how you manage Terraform.
Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.
module "ec2_instance" {
source = "soc2.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "pcidss.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "hipaa.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nist80053.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "fedrampmoderate.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cisv80ig1.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nist800171.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cisacyberessentials.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nydfs23.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "ffiec.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cfrpart11.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "rbicybersecurity.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "rbiitfnbfc.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "fedramplow.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "hipaasecurity2003.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nistcsfv11.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nist80053rev4.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "pcidssv321.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
If you use terraform-aws-modules/ec2-instance/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.
module "ec2_instance" {
source = "terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
associate_public_ip_address = false
}
Use AWS provider resources directly. See docs for the resources involved: aws_instance.
resource "aws_instance" "this" {
ami = "ami-abc12345"
instance_type = "t4g.nano"
subnet_id = element(["subnet-abc123", "subnet-def456"], 0)
vpc_security_group_ids = ["sg-abc12345"]
associate_public_ip_address = false
}
What this control checks
This control validates that EC2 instances are not assigned public IP addresses. In Terraform, aws_instance has an associate_public_ip_address argument that must be false or omitted, but only when the instance launches in a subnet where map_public_ip_on_launch is false. If the subnet sets map_public_ip_on_launch = true, the instance inherits a public IP regardless of what aws_instance sets.
To pass: aws_subnet resources used for EC2 instances must set map_public_ip_on_launch = false, and no aws_instance may set associate_public_ip_address = true. No aws_eip_association or aws_eip with an instance argument may point to the instance, as an Elastic IP also makes it publicly reachable. For launch templates, the network_interfaces block inside aws_launch_template must set associate_public_ip_address = false explicitly.
Common pitfalls
Subnet auto-assign overrides instance setting
The subnet-level default wins. If
map_public_ip_on_launch = trueonaws_subnet, every instance in that subnet gets a public IP regardless of whetherassociate_public_ip_addressis set onaws_instance. You needmap_public_ip_on_launch = falseon the subnet, not just silence at the instance level.Elastic IPs bypass the check at launch time
An instance can launch without a public IP and still become publicly accessible when an
aws_eipis attached viaaws_eip_association. Plan-time policy checks often miss this when the EIP association lives in a separate module or gets applied after the initial deployment.Launch templates with inline network interfaces
When using
aws_launch_templatewith anetwork_interfacesblock,associate_public_ip_addressgoes inside that block, not at the top level. Omit it there and the instance inherits whatever the subnet defaults to, which may be public. Auto Scaling groups using these templates can silently launch public instances at scale.Legacy inline security group pattern obscures subnet context
Older configs that use
vpc_security_group_idsalongside asubnet_idpointing to a public subnet won't make the exposure obvious during code review. The only way to know is to check the referenced subnet'smap_public_ip_on_launchvalue.
Audit evidence
Config rule ec2-instance-no-public-ip evaluation results showing all instances as COMPLIANT are the primary evidence. Support with aws ec2 describe-instances output filtered to confirm PublicIpAddress and PublicDnsName are empty across all instances, and VPC Flow Logs showing instances communicate only over private IPs.
EC2 console screenshots showing the "Public IPv4 address" column blank for all instances, plus subnet configuration showing Auto-assign public IPv4 address set to "No," round out the package. Any exceptions, a bastion host for instance, need documented risk acceptances with compensating controls.
Framework-specific interpretation
SOC 2: CC6.1 and CC6.6 call for logical access controls over network boundaries. Removing public IP assignment from EC2 instances is direct evidence that the organization restricts ingress paths rather than relying on security groups alone.
PCI DSS v4.0: For environments in or connected to the CDE, Requirement 1.3 says inbound and outbound traffic must be restricted to what the business needs. Instances without public IPs can't receive direct inbound connections from untrusted networks, which is the segmentation model Requirement 1.3 expects.
HIPAA Omnibus Rule 2013: EC2 instances processing ePHI must not be directly reachable from the internet. Keeping instances in private subnets satisfies the 45 CFR 164.312(a)(1) technical safeguard for access controls that limit electronic access to authorized users and processes.
NIST SP 800-53 Rev 5: SC-7 (Boundary Protection) and AC-4 (Information Flow Enforcement) both apply. Removing public IPs means traffic to EC2 must traverse monitored choke points like load balancers or VPN gateways, which is exactly what boundary protection calls for.
FedRAMP Moderate Baseline Rev 4: At the Moderate baseline, SC-7 requires that federal information systems are not directly reachable from the internet without boundary device mediation. Private IP-only instances are the standard implementation of that requirement.
Related controls
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
Compliance.tf Control:
ec2_instance_not_publicly_accessibleAWS Config Managed Rule:
EC2_INSTANCE_NO_PUBLIC_IPCheckov Check:
CKV_AWS_88Powerpipe Control:
aws_compliance.control.ec2_instance_not_publicly_accessibleProwler Check:
ec2_instance_public_ipAWS Security Hub Control:
EC2.9KICS Query:
5a2486aa-facf-477d-a5c1-b010789459ceTrivy Check:
AWS-0164
Last reviewed: 2026-03-09