Best Practice: Network Security & Segmentation
Context
Network segmentation is the technical implementation of the defense-in-depth principle at the network level. Without segmentation, a compromised system can reach all other systems in the same network.
Typical misconfigurations:
-
Flat network: all resources in one VPC without subnet separation
-
Security groups with
0.0.0.0/0on management ports (SSH 22, RDP 3389) -
Databases in public subnets with
publicly_accessible = true -
Missing VPC flow logs (no network forensics possible)
-
S3, KMS, SQS accessed over the internet instead of VPC endpoints
Related Controls
-
WAF-SEC-050 – Network Segmentation & Security Group Hardening
VPC Design: Private by Default
Recommended Subnet Structure
# Three-tier VPC design for production workloads
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "prod-vpc"
}
}
# Public subnets: ONLY for load balancers and NAT gateways
resource "aws_subnet" "public" {
for_each = {
"a" = "10.0.0.0/24"
"b" = "10.0.1.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = "eu-central-1${each.key}"
map_public_ip_on_launch = false # No automatic public IPs!
tags = { Name = "public-${each.key}" }
}
# Private subnets: Application tier
resource "aws_subnet" "private_app" {
for_each = {
"a" = "10.0.10.0/24"
"b" = "10.0.11.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = "eu-central-1${each.key}"
tags = { Name = "private-app-${each.key}" }
}
# Private subnets: Data tier (isolated!)
resource "aws_subnet" "private_data" {
for_each = {
"a" = "10.0.20.0/24"
"b" = "10.0.21.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = "eu-central-1${each.key}"
tags = { Name = "private-data-${each.key}" }
}
Security Group Strategy: Application-First
Security groups are structured by application logic, not port numbers:
# ALB security group: HTTPS only from internet
resource "aws_security_group" "alb" {
name = "alb-public"
description = "Allow HTTPS inbound from internet to ALB"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS from internet"
}
# No open egress – only to app tier
egress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.app.id]
description = "Forward to application tier"
}
}
# Application security group: only from ALB
resource "aws_security_group" "app" {
name = "app-tier"
description = "Application tier: only from ALB"
vpc_id = aws_vpc.main.id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "From ALB only"
}
# Egress: only to database and AWS services (via VPC endpoint)
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.db.id]
description = "To PostgreSQL"
}
}
# Database security group: only from application tier
resource "aws_security_group" "db" {
name = "db-tier"
description = "Database tier: only from app tier"
vpc_id = aws_vpc.main.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
description = "From application tier only"
}
# No egress needed for database tier
}
| Compliant | Non-Compliant |
|---|---|
|
|
NACLs as a Second Layer of Defense
Network ACLs (NACLs) operate at the subnet level and complement security groups:
# NACL for data tier subnets: only from app tier
resource "aws_network_acl" "data_tier" {
vpc_id = aws_vpc.main.id
subnet_ids = [for subnet in aws_subnet.private_data : subnet.id]
# Inbound: only from app tier CIDR
ingress {
rule_no = 100
action = "allow"
protocol = "tcp"
from_port = 5432
to_port = 5432
cidr_block = "10.0.10.0/23" # App tier subnets
}
# Deny all other inbound connections
ingress {
rule_no = 32766
action = "deny"
protocol = "-1"
from_port = 0
to_port = 0
cidr_block = "0.0.0.0/0"
}
# Outbound: only response traffic (ephemeral ports)
egress {
rule_no = 100
action = "allow"
protocol = "tcp"
from_port = 1024
to_port = 65535
cidr_block = "10.0.10.0/23"
}
egress {
rule_no = 32766
action = "deny"
protocol = "-1"
from_port = 0
to_port = 0
cidr_block = "0.0.0.0/0"
}
}
VPC Flow Logs: Network Forensics
VPC flow logs record all network connections and are indispensable for security forensics and anomaly detection:
# CloudWatch log group for VPC flow logs
resource "aws_cloudwatch_log_group" "vpc_flow_logs" {
name = "/aws/vpc/flow-logs/prod"
retention_in_days = 90
kms_key_id = aws_kms_key.logs.arn # Encrypt with CMK
}
# IAM role for flow logs
resource "aws_iam_role" "flow_logs" {
name = "vpc-flow-logs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "vpc-flow-logs.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
# Enable VPC flow logs (all traffic types)
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL" # ACCEPT, REJECT, or ALL
iam_role_arn = aws_iam_role.flow_logs.arn
log_destination = aws_cloudwatch_log_group.vpc_flow_logs.arn
# Extended fields for better forensics
log_format = "$${version} $${account-id} $${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport} $${protocol} $${packets} $${bytes} $${start} $${end} $${action} $${log-status} $${vpc-id} $${subnet-id} $${instance-id} $${tcp-flags}"
}
VPC Endpoints: Egress via AWS Backbone
Instead of accessing S3, KMS, SQS over the public internet, VPC endpoints are used:
# Gateway endpoint for S3 (free, no NAT gateway needed)
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.eu-central-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
}
# Interface endpoint for Secrets Manager
resource "aws_vpc_endpoint" "secrets_manager" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.eu-central-1.secretsmanager"
vpc_endpoint_type = "Interface"
subnet_ids = [for subnet in aws_subnet.private_app : subnet.id]
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
# Interface endpoint for KMS
resource "aws_vpc_endpoint" "kms" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.eu-central-1.kms"
vpc_endpoint_type = "Interface"
subnet_ids = [for subnet in aws_subnet.private_app : subnet.id]
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
Benefits of VPC endpoints:
-
No NAT gateway traffic → direct cost savings
-
Traffic never leaves the AWS network → better data sovereignty
-
Restrictive egress policies possible (security groups without internet egress)
Anti-Patterns
-
Flat network: One subnet for everything – no isolation possible.
-
Open security groups:
0.0.0.0/0on SSH, RDP, or admin ports. -
Missing VPC flow logs: No network forensics possible after incidents.
-
Databases without
publicly_accessible = false: Publicly reachable databases are unacceptable. -
No NAT gateway for private subnets: Workaround via public IPs is worse.
-
Too broad security group egress:
0.0.0.0/0egress opens unnecessary attack surface.