AWS VPC Network Segmentation Best Practices

Complete guide to AWS VPC network segmentation covering public and private subnet design, security groups, NACLs, VPC peering, Transit Gateway, and defense-in-depth network architecture.

11 min readUpdated 2026-01-13

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

ComponentPurposeScope
VPCIsolated virtual networkRegion
SubnetIP address range segmentAvailability Zone
Route TableTraffic routing rulesSubnet
Security GroupStateful instance firewallInstance/ENI
NACLStateless subnet firewallSubnet
Internet GatewayInternet connectivityVPC
NAT GatewayOutbound internet for private subnetsAvailability 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, VPN

Create 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-public1a

Private 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-private1a

Security 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/0

Application 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-alb123

Database 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-app123

Network 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-data123

Terraform 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-enabled

Terraform 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-role

Best Practices Summary

PracticeRecommendation
Subnet DesignSeparate public, private, and data tiers
Security GroupsReference other SGs instead of CIDRs
NACLsAdd as extra layer for sensitive subnets
VPC EndpointsUse for all frequently-accessed AWS services
Flow LogsEnable on all VPCs for troubleshooting
High AvailabilityDeploy across minimum 2-3 AZs
NAT GatewayOne per AZ for production workloads

Frequently Asked Questions

Find answers to common questions

Security groups are stateful firewalls at the instance level. They automatically allow return traffic, only support allow rules, and are evaluated as a whole. NACLs (Network Access Control Lists) are stateless firewalls at the subnet level. They require explicit rules for both inbound and outbound traffic, support both allow and deny rules, and are evaluated in order by rule number. Use security groups for most access control and NACLs as an additional layer for subnet-level protection.

Need Professional Help?

Our team of experts can help you implement and configure these solutions for your organization.