Back to Checks
aws

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 Cloud
Run
prowler aws --checks vpc_subnet_separate_private_public

Recommendation

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 subnet

Other

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