Back to Checks
VPC has both public and private subnets
vpc_subnet_separate_private_public
Service: vpcSeverity: medium
OfficialBuilt-inVerified
Categories:trust-boundaries
Resource Type:AwsEc2Subnet
Amazon VPCs are assessed for network segmentation: at least one public subnet (internet-routable) and one private subnet (non-internet-routable).
It flags VPCs with no subnets, only public subnets, or only private subnets.
Risk
Missing subnet separation erodes segmentation.
- Only public: workloads face Internet exposure, enabling scanning, brute force, and lateral movement, threatening confidentiality and integrity.
- Only private: no controlled egress can break patching and dependencies, impacting availability.
- No subnets: misconfiguration leaves services unreachable.
Run this check with Prowler
Run in Prowler CloudRun
prowler aws --checks vpc_subnet_separate_private_publicRecommendation
Segment each VPC: put internet-facing endpoints in public subnets and internal workloads in private subnets. Restrict ingress/egress with tight route tables, NACLs, and security groups, minimizing 0.0.0.0/0. Apply least privilege and defense in depth. Provide controlled outbound for private subnets via managed egress and use hardened admin access patterns.
Remediation
Terraform
# Terraform: add one public and one private subnet to a VPC
resource "aws_internet_gateway" "<example_resource_name>" {
vpc_id = "<example_resource_id>"
}
resource "aws_subnet" "public_<example_resource_name>" {
vpc_id = "<example_resource_id>"
cidr_block = "10.0.1.0/24"
}
resource "aws_subnet" "private_<example_resource_name>" {
vpc_id = "<example_resource_id>"
cidr_block = "10.0.2.0/24"
}
resource "aws_route_table" "public_<example_resource_name>" {
vpc_id = "<example_resource_id>"
route { # CRITICAL: makes subnet public via IGW
cidr_block = "0.0.0.0/0" # default route to Internet
gateway_id = aws_internet_gateway.<example_resource_name>.id
}
}
resource "aws_route_table_association" "public_assoc_<example_resource_name>" {
subnet_id = aws_subnet.public_<example_resource_name>.id
route_table_id = aws_route_table.public_<example_resource_name>.id # CRITICAL
}
resource "aws_route_table" "private_<example_resource_name>" {
vpc_id = "<example_resource_id>" # CRITICAL: no IGW route keeps subnet private
}
resource "aws_route_table_association" "private_assoc_<example_resource_name>" {
subnet_id = aws_subnet.private_<example_resource_name>.id
route_table_id = aws_route_table.private_<example_resource_name>.id # CRITICAL
}NativeIaC
# CloudFormation: add one public and one private subnet to a VPC
Resources:
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: <example_resource_id>
InternetGatewayId: !Ref InternetGateway
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: <example_resource_id>
CidrBlock: 10.0.1.0/24 # creates a subnet to be made public via route
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: <example_resource_id>
CidrBlock: 10.0.2.0/24 # creates a subnet that remains private (no IGW route)
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: <example_resource_id>
PublicDefaultRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0 # CRITICAL: makes routes to the Internet
GatewayId: !Ref InternetGateway # CRITICAL: via Internet Gateway
PublicAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable # CRITICAL: marks this subnet as public
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: <example_resource_id>
PrivateAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable # CRITICAL: no IGW route -> private subnetOther
1In the AWS console, go to VPC > Your VPCs and select the failing VPC
2Attach an Internet Gateway if none exists: Internet Gateways > Create > Attach to the VPC
3Create two subnets: Subnets > Create subnet
4- Subnet A (public): CIDR e.g., 10.0.1.0/24
5- Subnet B (private): CIDR e.g., 10.0.2.0/24
6Create a route table for public subnet: Route tables > Create
7- Add route: 0.0.0.0/0 -> Internet Gateway (IGW)
8- Associate this route table to Subnet A (public)
9Create a route table for private subnet: Route tables > Create
10- Do not add a route to an Internet Gateway
11- Associate this route table to Subnet B (private)
12Verify the VPC now has at least one subnet with an IGW route (public) and one without (private)
Check Code
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.vpc.vpc_client import vpc_client
class vpc_subnet_separate_private_public(Check):
def execute(self):
findings = []
for vpc in vpc_client.vpcs.values():
if vpc_client.provider.scan_unused_services or vpc.in_use:
report = Check_Report_AWS(metadata=self.metadata(), resource=vpc)
report.status = "FAIL"
report.status_extended = (
f"VPC {vpc.name if vpc.name else vpc.id} has no subnets."
)
if vpc.subnets:
public = False
private = False
for subnet in vpc.subnets:
if subnet.public:
public = True
report.status_extended = f"VPC {vpc.name if vpc.name else vpc.id} has only public subnets."
if not subnet.public:
private = True
report.status_extended = f"VPC {vpc.name if vpc.name else vpc.id} has only private subnets."
if public and private:
report.status = "PASS"
report.status_extended = f"VPC {vpc.name if vpc.name else vpc.id} has private and public subnets."
findings.append(report)
return findings