Many developers assume that placing resources in a public subnet without Elastic IPs provides equivalent security to private subnets. While technically true for inbound traffic, this overlooks critical architectural differences:
1. Explicit Outbound Control:
Private subnets require NAT gateways for outbound internet access, creating an explicit security boundary:
# Public subnet route table
0.0.0.0/0 -> igw-12345
# Private subnet route table
0.0.0.0/0 -> nat-67890
2. Defense in Depth:
Even if you don't assign Elastic IPs, public subnet instances can still:
- Initiate outbound connections freely
- Be accidentally exposed via auto-assigned public IPs
- Become vulnerable through misconfigured security groups
Here's how major architectures leverage private subnets:
# Terraform example showing proper isolation
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "DB-Tier-Private"
Tier = "internal"
}
}
resource "aws_network_acl" "private" {
vpc_id = aws_vpc.main.id
subnet_ids = [aws_subnet.private.id]
egress {
protocol = "-1"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
}
}
For simple non-production environments or when using:
- VPC endpoints for AWS services
- No outbound internet requirements
- Temporary development stacks
Industry standards like CIS AWS Foundations recommend:
"Ensure no resources in private subnets have direct internet access except through controlled NAT"
This becomes impossible to enforce without proper subnet segregation.
Private subnets enable:
Feature | Public Subnet | Private Subnet |
---|---|---|
NAT Gateway | Not required | Optional |
Data Transfer | Potentially more expensive | Internal VPC rates |
While it's true that EC2 instances in public subnets aren't internet-accessible without Elastic IPs, private subnets provide additional layers of security through architectural enforcement:
// Security Group for Private Subnet (denies ALL inbound from 0.0.0.0/0)
resource "aws_security_group" "db_sg" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.main.cidr_block] // Only allow from VPC
}
}
Private subnets enable controlled outbound internet access through NAT while maintaining inbound isolation:
# Terraform NAT Gateway configuration
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id # NAT must be in public subnet
}
When using auto-scaling groups, private subnets prevent accidental exposure during scaling events:
// Auto Scaling Group in private subnet
resource "aws_autoscaling_group" "db_asg" {
vpc_zone_identifier = [aws_subnet.private1.id, aws_subnet.private2.id]
launch_template {
id = aws_launch_template.db.id
version = "$Latest"
}
}
Many compliance frameworks (HIPAA, PCI DSS) explicitly require private subnet architectures for sensitive workloads.
# AWS Config rule checking for private subnet compliance
resource "aws_config_organization_managed_rule" "private_subnet_validation" {
name = "restricted-ssh"
rule_identifier = "EC2_SECURITY_GROUPS_RESTRICTED_INCOMING_TRAFFIC"
input_parameters = jsonencode({
blockedPort1 = "22"
blockedPort2 = "3389"
})
}
Segregating traffic between public/private subnets provides cleaner monitoring:
resource "aws_flow_log" "vpc_flow_log" {
log_destination = aws_s3_bucket.flow_log_bucket.arn
traffic_type = "ALL"
vpc_id = aws_vpc.main.id
log_destination_type = "s3"
}
Here's a complete three-tier architecture implementation:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
cidr = "10.0.0.0/16"
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
}