Check ID: vpc_subnet_separate_private_public
Provider: AWS
Service: vpc
Severity: MEDIUM
Categories: trust-boundaries
Resource type: AwsEc2Subnet

What this check verifies

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.

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: 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
}

References

Where this check fires

This check runs on every scheduled scan against your AWS account. Findings appear at Cloud Security → Findings filterable by Check ID = vpc_subnet_separate_private_public. To re-evaluate after a fix, hit Run now on the account’s schedule under Cloud Security → Schedules — the next scan re-checks this control and marks the finding fixed or keeps it persistent.
← Back to AWS checks