VPC network segmentation is a foundational security control that limits blast radius, enforces least-privilege access, and protects sensitive workloads. Proper network architecture in AWS prevents lateral movement if attackers breach one component and ensures compliance with data isolation requirements.
This article is part of our comprehensive Cloud Security Tips for 2026 guide covering essential practices for protecting your cloud environment.
VPC Architecture Overview
| Component | Purpose | Scope |
|---|---|---|
| VPC | Isolated virtual network | Region |
| Subnet | IP address range segment | Availability Zone |
| Route Table | Traffic routing rules | Subnet |
| Security Group | Stateful instance firewall | Instance/ENI |
| NACL | Stateless subnet firewall | Subnet |
| Internet Gateway | Internet connectivity | VPC |
| NAT Gateway | Outbound internet for private subnets | Availability Zone |
Design a Secure VPC Architecture
Reference Architecture
VPC CIDR: 10.0.0.0/16
├── Public Subnets (Internet-facing)
│ ├── 10.0.1.0/24 (us-east-1a) - ALB, NAT Gateway
│ ├── 10.0.2.0/24 (us-east-1b) - ALB, NAT Gateway
│ └── 10.0.3.0/24 (us-east-1c) - ALB, NAT Gateway
│
├── Private Subnets (Application tier)
│ ├── 10.0.11.0/24 (us-east-1a) - App servers
│ ├── 10.0.12.0/24 (us-east-1b) - App servers
│ └── 10.0.13.0/24 (us-east-1c) - App servers
│
├── Data Subnets (Database tier)
│ ├── 10.0.21.0/24 (us-east-1a) - RDS, ElastiCache
│ ├── 10.0.22.0/24 (us-east-1b) - RDS, ElastiCache
│ └── 10.0.23.0/24 (us-east-1c) - RDS, ElastiCache
│
└── Management Subnets (Ops/Admin)
├── 10.0.31.0/24 (us-east-1a) - Bastion, VPN
└── 10.0.32.0/24 (us-east-1b) - Bastion, VPNCreate VPC with Subnets
# Create VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=production-vpc}]'
# Enable DNS hostnames
aws ec2 modify-vpc-attribute \
--vpc-id vpc-12345678 \
--enable-dns-hostnames
# Create Internet Gateway
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=production-igw}]'
aws ec2 attach-internet-gateway \
--vpc-id vpc-12345678 \
--internet-gateway-id igw-12345678
# Create Public Subnet
aws ec2 create-subnet \
--vpc-id vpc-12345678 \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-1a}]'
# Create Private Subnet
aws ec2 create-subnet \
--vpc-id vpc-12345678 \
--cidr-block 10.0.11.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-app-1a}]'
# Create Data Subnet
aws ec2 create-subnet \
--vpc-id vpc-12345678 \
--cidr-block 10.0.21.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-data-1a}]'Configure Route Tables
Public Subnet Route Table
# Create public route table
aws ec2 create-route-table \
--vpc-id vpc-12345678 \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=public-rt}]'
# Add route to Internet Gateway
aws ec2 create-route \
--route-table-id rtb-public123 \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id igw-12345678
# Associate with public subnets
aws ec2 associate-route-table \
--route-table-id rtb-public123 \
--subnet-id subnet-public1aPrivate Subnet Route Table (with NAT Gateway)
# Allocate Elastic IP for NAT Gateway
aws ec2 allocate-address --domain vpc
# Create NAT Gateway in public subnet
aws ec2 create-nat-gateway \
--subnet-id subnet-public1a \
--allocation-id eipalloc-12345678 \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=nat-1a}]'
# Create private route table
aws ec2 create-route-table \
--vpc-id vpc-12345678 \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=private-rt-1a}]'
# Add route to NAT Gateway
aws ec2 create-route \
--route-table-id rtb-private1a \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-12345678
# Associate with private subnets
aws ec2 associate-route-table \
--route-table-id rtb-private1a \
--subnet-id subnet-private1aSecurity Groups (Stateful Firewall)
Application Load Balancer Security Group
# Create ALB security group
aws ec2 create-security-group \
--group-name alb-sg \
--description "Security group for Application Load Balancer" \
--vpc-id vpc-12345678
# Allow HTTPS from internet
aws ec2 authorize-security-group-ingress \
--group-id sg-alb123 \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Allow HTTP for redirect
aws ec2 authorize-security-group-ingress \
--group-id sg-alb123 \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0Application Server Security Group
# Create app server security group
aws ec2 create-security-group \
--group-name app-sg \
--description "Security group for application servers" \
--vpc-id vpc-12345678
# Allow traffic only from ALB security group
aws ec2 authorize-security-group-ingress \
--group-id sg-app123 \
--protocol tcp \
--port 8080 \
--source-group sg-alb123Database Security Group
# Create database security group
aws ec2 create-security-group \
--group-name db-sg \
--description "Security group for databases" \
--vpc-id vpc-12345678
# Allow PostgreSQL only from app servers
aws ec2 authorize-security-group-ingress \
--group-id sg-db123 \
--protocol tcp \
--port 5432 \
--source-group sg-app123Network ACLs (Stateless Firewall)
NACLs provide an additional layer of defense at the subnet level:
# Create NACL for data subnets
aws ec2 create-network-acl \
--vpc-id vpc-12345678 \
--tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=data-nacl}]'
# Allow PostgreSQL inbound from app subnets
aws ec2 create-network-acl-entry \
--network-acl-id acl-data123 \
--ingress \
--rule-number 100 \
--protocol tcp \
--port-range From=5432,To=5432 \
--cidr-block 10.0.11.0/24 \
--rule-action allow
# Allow ephemeral ports for return traffic
aws ec2 create-network-acl-entry \
--network-acl-id acl-data123 \
--egress \
--rule-number 100 \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 10.0.11.0/24 \
--rule-action allow
# Deny all other inbound traffic
aws ec2 create-network-acl-entry \
--network-acl-id acl-data123 \
--ingress \
--rule-number 200 \
--protocol -1 \
--cidr-block 0.0.0.0/0 \
--rule-action deny
# Associate NACL with data subnets
aws ec2 replace-network-acl-association \
--association-id aclassoc-old123 \
--network-acl-id acl-data123Terraform Configuration
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
database_subnets = ["10.0.21.0/24", "10.0.22.0/24", "10.0.23.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # One per AZ for HA
one_nat_gateway_per_az = true
enable_dns_hostnames = true
enable_dns_support = true
# VPC Flow Logs
enable_flow_log = true
create_flow_log_cloudwatch_log_group = true
create_flow_log_cloudwatch_iam_role = true
flow_log_max_aggregation_interval = 60
tags = {
Environment = "production"
}
}
# ALB Security Group
resource "aws_security_group" "alb" {
name = "alb-sg"
description = "Security group for ALB"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
}
# App Security Group
resource "aws_security_group" "app" {
name = "app-sg"
description = "Security group for app servers"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.db.id]
}
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Database Security Group
resource "aws_security_group" "db" {
name = "db-sg"
description = "Security group for databases"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
}VPC Endpoints for Private Connectivity
# Create S3 Gateway Endpoint (free)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-private1a rtb-private1b
# Create ECR Interface Endpoint
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.us-east-1.ecr.api \
--subnet-ids subnet-private1a subnet-private1b \
--security-group-ids sg-endpoint123
# Create CloudWatch Logs Endpoint
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.us-east-1.logs \
--subnet-ids subnet-private1a subnet-private1b \
--security-group-ids sg-endpoint123 \
--private-dns-enabledTerraform VPC Endpoints
# Gateway endpoints (free)
resource "aws_vpc_endpoint" "s3" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.us-east-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = module.vpc.private_route_table_ids
}
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.us-east-1.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = module.vpc.private_route_table_ids
}
# Interface endpoints
resource "aws_vpc_endpoint" "ecr_api" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.us-east-1.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = module.vpc.private_subnets
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
resource "aws_security_group" "vpc_endpoints" {
name = "vpc-endpoints-sg"
description = "Security group for VPC endpoints"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [module.vpc.vpc_cidr_block]
}
}Enable VPC Flow Logs
# Create CloudWatch log group
aws logs create-log-group --log-group-name /aws/vpc/flow-logs
# Create IAM role for VPC Flow Logs
aws iam create-role \
--role-name vpc-flow-logs-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "vpc-flow-logs.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach policy
aws iam put-role-policy \
--role-name vpc-flow-logs-role \
--policy-name vpc-flow-logs-policy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
],
"Resource": "*"
}]
}'
# Create VPC Flow Log
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-12345678 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /aws/vpc/flow-logs \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/vpc-flow-logs-roleBest Practices Summary
| Practice | Recommendation |
|---|---|
| Subnet Design | Separate public, private, and data tiers |
| Security Groups | Reference other SGs instead of CIDRs |
| NACLs | Add as extra layer for sensitive subnets |
| VPC Endpoints | Use for all frequently-accessed AWS services |
| Flow Logs | Enable on all VPCs for troubleshooting |
| High Availability | Deploy across minimum 2-3 AZs |
| NAT Gateway | One per AZ for production workloads |