AWS VPC Security Architecture: Building Fortress-Level Network Security
VPC security architecture forms the foundation of AWS network security, yet many organizations treat it as an afterthought. They focus on application-layer security while neglecting the network controls that could prevent attacks from reaching their applications in the first place.
The challenge with VPC security is that it requires thinking in layers and understanding complex interactions between multiple security controls. A single misconfigured route table, overpermissive security group, or missing network ACL can create security gaps that attackers can exploit. And as your AWS environment grows across multiple VPCs and accounts, these complexities multiply exponentially.
This comprehensive guide explores advanced patterns for building secure, scalable, and maintainable VPC architectures that protect against modern threats. We'll cover defense-in-depth strategies, network segmentation patterns, and monitoring approaches that provide comprehensive network security at enterprise scale.
VPC Security Architecture Principles
Defense in Depth Strategy
# Multi-layer security architecture
Security Layers:
1. Network ACLs (Subnet level)
2. Security Groups (Instance level)
3. WAF (Application level)
4. Host-based firewalls
5. Application security controls
Network Segmentation Patterns
{
"VPCArchitecture": {
"PublicSubnets": {
"Purpose": "Load balancers, NAT gateways, bastion hosts",
"InternetAccess": "Bidirectional",
"SecurityControls": ["NACLs", "SecurityGroups", "WAF"]
},
"PrivateSubnets": {
"Purpose": "Application servers, databases",
"InternetAccess": "Outbound only via NAT",
"SecurityControls": ["NACLs", "SecurityGroups", "VPC Endpoints"]
},
"IsolatedSubnets": {
"Purpose": "Highly sensitive workloads",
"InternetAccess": "None",
"SecurityControls": ["NACLs", "SecurityGroups", "Private endpoints"]
}
}
}
Advanced VPC Security Patterns
Hub-and-Spoke Architecture
import boto3
def create_hub_spoke_architecture():
ec2 = boto3.client('ec2')
# Create hub VPC (shared services)
hub_vpc = ec2.create_vpc(
CidrBlock='10.0.0.0/16',
TagSpecifications=[{
'ResourceType': 'vpc',
'Tags': [
{'Key': 'Name', 'Value': 'hub-vpc'},
{'Key': 'Purpose', 'Value': 'shared-services'}
]
}]
)
# Create spoke VPCs (workload-specific)
spoke_vpcs = []
workloads = ['production', 'staging', 'development']
for i, workload in enumerate(workloads):
spoke_vpc = ec2.create_vpc(
CidrBlock=f'10.{i+1}.0.0/16',
TagSpecifications=[{
'ResourceType': 'vpc',
'Tags': [
{'Key': 'Name', 'Value': f'{workload}-vpc'},
{'Key': 'Environment', 'Value': workload}
]
}]
)
spoke_vpcs.append(spoke_vpc)
# Create Transit Gateway for connectivity
tgw = ec2.create_transit_gateway(
Description='Hub-spoke transit gateway',
Options={
'AmazonSideAsn': 64512,
'DefaultRouteTableAssociation': 'disable',
'DefaultRouteTablePropagation': 'disable'
},
TagSpecifications=[{
'ResourceType': 'transit-gateway',
'Tags': [{'Key': 'Name', 'Value': 'hub-spoke-tgw'}]
}]
)
return hub_vpc, spoke_vpcs, tgw
def implement_segmented_routing(tgw_id, vpc_attachments):
"""Implement segmented routing with custom route tables"""
ec2 = boto3.client('ec2')
# Create route tables for different traffic patterns
route_tables = {
'shared-services': ec2.create_transit_gateway_route_table(
TransitGatewayId=tgw_id,
TagSpecifications=[{
'ResourceType': 'transit-gateway-route-table',
'Tags': [{'Key': 'Name', 'Value': 'shared-services-rt'}]
}]
),
'production': ec2.create_transit_gateway_route_table(
TransitGatewayId=tgw_id,
TagSpecifications=[{
'ResourceType': 'transit-gateway-route-table',
'Tags': [{'Key': 'Name', 'Value': 'production-rt'}]
}]
),
'non-production': ec2.create_transit_gateway_route_table(
TransitGatewayId=tgw_id,
TagSpecifications=[{
'ResourceType': 'transit-gateway-route-table',
'Tags': [{'Key': 'Name', 'Value': 'non-production-rt'}]
}]
)
}
# Associate and propagate routes based on security requirements
for attachment in vpc_attachments:
environment = attachment['environment']
if environment == 'production':
# Production can only communicate with shared services
ec2.associate_transit_gateway_route_table(
TransitGatewayAttachmentId=attachment['id'],
TransitGatewayRouteTableId=route_tables['production']['TransitGatewayRouteTable']['TransitGatewayRouteTableId']
)
else:
# Non-production environments can communicate with each other
ec2.associate_transit_gateway_route_table(
TransitGatewayAttachmentId=attachment['id'],
TransitGatewayRouteTableId=route_tables['non-production']['TransitGatewayRouteTable']['TransitGatewayRouteTableId']
)
Micro-Segmentation with Security Groups
def create_microsegmentation_security_groups():
"""Create fine-grained security groups for micro-segmentation"""
ec2 = boto3.client('ec2')
# Application tier security groups
security_groups = {
'web-tier': {
'description': 'Web tier - accepts traffic from ALB only',
'ingress_rules': [
{
'IpProtocol': 'tcp',
'FromPort': 80,
'ToPort': 80,
'UserIdGroupPairs': [{'GroupId': 'sg-alb'}]
},
{
'IpProtocol': 'tcp',
'FromPort': 443,
'ToPort': 443,
'UserIdGroupPairs': [{'GroupId': 'sg-alb'}]
}
]
},
'app-tier': {
'description': 'Application tier - accepts traffic from web tier only',
'ingress_rules': [
{
'IpProtocol': 'tcp',
'FromPort': 8080,
'ToPort': 8080,
'UserIdGroupPairs': [{'GroupId': 'sg-web-tier'}]
}
]
},
'db-tier': {
'description': 'Database tier - accepts traffic from app tier only',
'ingress_rules': [
{
'IpProtocol': 'tcp',
'FromPort': 3306,
'ToPort': 3306,
'UserIdGroupPairs': [{'GroupId': 'sg-app-tier'}]
}
]
},
'management': {
'description': 'Management access - restricted to admin networks',
'ingress_rules': [
{
'IpProtocol': 'tcp',
'FromPort': 22,
'ToPort': 22,
'IpRanges': [{'CidrIp': '10.0.100.0/24', 'Description': 'Admin subnet'}]
}
]
}
}
created_sgs = {}
for sg_name, config in security_groups.items():
sg = ec2.create_security_group(
GroupName=sg_name,
Description=config['description'],
VpcId='vpc-12345678'
)
# Add ingress rules
if config['ingress_rules']:
ec2.authorize_security_group_ingress(
GroupId=sg['GroupId'],
IpPermissions=config['ingress_rules']
)
created_sgs[sg_name] = sg['GroupId']
return created_sgs
Network Access Control Lists (NACLs)
Stateless Firewall Rules
def create_security_focused_nacls():
"""Create NACLs with security-first approach"""
ec2 = boto3.client('ec2')
# Public subnet NACL - restrictive inbound, permissive outbound
public_nacl_rules = [
# Inbound rules
{
'RuleNumber': 100,
'Protocol': '6', # TCP
'RuleAction': 'allow',
'PortRange': {'From': 80, 'To': 80},
'CidrBlock': '0.0.0.0/0'
},
{
'RuleNumber': 110,
'Protocol': '6', # TCP
'RuleAction': 'allow',
'PortRange': {'From': 443, 'To': 443},
'CidrBlock': '0.0.0.0/0'
},
{
'RuleNumber': 120,
'Protocol': '6', # TCP
'RuleAction': 'allow',
'PortRange': {'From': 1024, 'To': 65535}, # Ephemeral ports
'CidrBlock': '0.0.0.0/0'
},
# Deny all other inbound traffic
{
'RuleNumber': 32767,
'Protocol': '-1',
'RuleAction': 'deny',
'CidrBlock': '0.0.0.0/0'
}
]
# Private subnet NACL - very restrictive
private_nacl_rules = [
# Allow traffic from public subnets only
{
'RuleNumber': 100,
'Protocol': '6', # TCP
'RuleAction': 'allow',
'PortRange': {'From': 80, 'To': 80},
'CidrBlock': '10.0.1.0/24' # Public subnet CIDR
},
{
'RuleNumber': 110,
'Protocol': '6', # TCP
'RuleAction': 'allow',
'PortRange': {'From': 443, 'To': 443},
'CidrBlock': '10.0.1.0/24'
},
# Allow return traffic
{
'RuleNumber': 120,
'Protocol': '6', # TCP
'RuleAction': 'allow',
'PortRange': {'From': 1024, 'To': 65535},
'CidrBlock': '0.0.0.0/0'
}
]
return public_nacl_rules, private_nacl_rules
def implement_nacl_logging():
"""Enable VPC Flow Logs for NACL monitoring"""
ec2 = boto3.client('ec2')
# Create flow log for rejected traffic
flow_log = ec2.create_flow_log(
ResourceType='VPC',
ResourceIds=['vpc-12345678'],
TrafficType='REJECT',
LogDestinationType='cloud-watch-logs',
LogGroupName='/aws/vpc/flowlogs',
DeliverLogsPermissionArn='arn:aws:iam::123456789012:role/flowlogsRole',
TagSpecifications=[{
'ResourceType': 'vpc-flow-log',
'Tags': [
{'Key': 'Name', 'Value': 'security-flow-logs'},
{'Key': 'Purpose', 'Value': 'security-monitoring'}
]
}]
)
return flow_log
VPC Endpoints for Secure Service Access
Private Connectivity Patterns
def create_vpc_endpoints_for_security():
"""Create VPC endpoints to avoid internet traffic"""
ec2 = boto3.client('ec2')
# Gateway endpoints (S3, DynamoDB)
gateway_endpoints = [
{
'ServiceName': 'com.amazonaws.us-east-1.s3',
'VpcEndpointType': 'Gateway',
'RouteTableIds': ['rtb-12345678']
},
{
'ServiceName': 'com.amazonaws.us-east-1.dynamodb',
'VpcEndpointType': 'Gateway',
'RouteTableIds': ['rtb-12345678']
}
]
# Interface endpoints (other AWS services)
interface_endpoints = [
{
'ServiceName': 'com.amazonaws.us-east-1.ec2',
'VpcEndpointType': 'Interface',
'SubnetIds': ['subnet-12345678'],
'SecurityGroupIds': ['sg-vpc-endpoints']
},
{
'ServiceName': 'com.amazonaws.us-east-1.ssm',
'VpcEndpointType': 'Interface',
'SubnetIds': ['subnet-12345678'],
'SecurityGroupIds': ['sg-vpc-endpoints']
},
{
'ServiceName': 'com.amazonaws.us-east-1.kms',
'VpcEndpointType': 'Interface',
'SubnetIds': ['subnet-12345678'],
'SecurityGroupIds': ['sg-vpc-endpoints']
}
]
created_endpoints = []
# Create gateway endpoints
for endpoint_config in gateway_endpoints:
endpoint = ec2.create_vpc_endpoint(
VpcId='vpc-12345678',
ServiceName=endpoint_config['ServiceName'],
VpcEndpointType=endpoint_config['VpcEndpointType'],
RouteTableIds=endpoint_config['RouteTableIds']
)
created_endpoints.append(endpoint)
# Create interface endpoints
for endpoint_config in interface_endpoints:
endpoint = ec2.create_vpc_endpoint(
VpcId='vpc-12345678',
ServiceName=endpoint_config['ServiceName'],
VpcEndpointType=endpoint_config['VpcEndpointType'],
SubnetIds=endpoint_config['SubnetIds'],
SecurityGroupIds=endpoint_config['SecurityGroupIds'],
PrivateDnsEnabled=True
)
created_endpoints.append(endpoint)
return created_endpoints
DNS Security in VPC
DNS Filtering and Monitoring
def implement_dns_security():
"""Implement DNS security controls"""
route53resolver = boto3.client('route53resolver')
# Create DNS firewall rule group
firewall_rule_group = route53resolver.create_firewall_rule_group(
Name='security-dns-firewall',
Tags=[
{'Key': 'Purpose', 'Value': 'DNS security filtering'}
]
)
# Block known malicious domains
malicious_domains = [
'malware-domain.com',
'phishing-site.net',
'command-control.org'
]
# Create domain list for blocking
domain_list = route53resolver.create_firewall_domain_list(
Name='blocked-domains',
Domains=malicious_domains,
Tags=[
{'Key': 'Purpose', 'Value': 'Malicious domain blocking'}
]
)
# Create firewall rule
firewall_rule = route53resolver.create_firewall_rule(
FirewallRuleGroupId=firewall_rule_group['FirewallRuleGroup']['Id'],
FirewallDomainListId=domain_list['FirewallDomainList']['Id'],
Priority=100,
Action='BLOCK',
BlockResponse='NXDOMAIN',
Name='block-malicious-domains'
)
# Associate with VPC
association = route53resolver.associate_firewall_rule_group(
FirewallRuleGroupId=firewall_rule_group['FirewallRuleGroup']['Id'],
VpcId='vpc-12345678',
Priority=100,
Name='vpc-dns-security'
)
return firewall_rule_group, domain_list, association
VPC Security Monitoring
Flow Log Analysis
def analyze_vpc_flow_logs():
"""Analyze VPC flow logs for security insights"""
# CloudWatch Insights query for suspicious traffic
suspicious_traffic_query = """
fields @timestamp, srcaddr, dstaddr, srcport, dstport, protocol, action
| filter action = "REJECT"
| stats count() by srcaddr
| sort count desc
| limit 20
"""
# Query for data exfiltration patterns
data_exfiltration_query = """
fields @timestamp, srcaddr, dstaddr, bytes
| filter bytes > 1000000
| stats sum(bytes) as total_bytes by srcaddr, dstaddr
| sort total_bytes desc
| limit 10
"""
# Query for port scanning
port_scanning_query = """
fields @timestamp, srcaddr, dstaddr, dstport, action
| filter action = "REJECT"
| stats count_distinct(dstport) as unique_ports by srcaddr
| sort unique_ports desc
| limit 10
"""
return {
'suspicious_traffic': suspicious_traffic_query,
'data_exfiltration': data_exfiltration_query,
'port_scanning': port_scanning_query
}
def create_security_dashboards():
"""Create CloudWatch dashboards for VPC security monitoring"""
cloudwatch = boto3.client('cloudwatch')
dashboard_body = {
"widgets": [
{
"type": "log",
"properties": {
"query": "SOURCE '/aws/vpc/flowlogs'\n| fields @timestamp, srcaddr, dstaddr, action\n| filter action = \"REJECT\"\n| stats count() by srcaddr\n| sort count desc\n| limit 20",
"region": "us-east-1",
"title": "Top Rejected Source IPs",
"view": "table"
}
},
{
"type": "metric",
"properties": {
"metrics": [
["AWS/VPC", "PacketsDroppedBySecurityGroup"],
[".", "PacketsDroppedByNetworkAcl"]
],
"period": 300,
"stat": "Sum",
"region": "us-east-1",
"title": "Dropped Packets"
}
}
]
}
dashboard = cloudwatch.put_dashboard(
DashboardName='VPC-Security-Monitoring',
DashboardBody=json.dumps(dashboard_body)
)
return dashboard
Automated Security Response
Lambda-based Incident Response
import json
import boto3
def lambda_handler(event, context):
"""Automated response to VPC security events"""
# Parse CloudWatch alarm
message = json.loads(event['Records'][0]['Sns']['Message'])
alarm_name = message['AlarmName']
if 'suspicious-traffic' in alarm_name.lower():
handle_suspicious_traffic(message)
elif 'port-scan' in alarm_name.lower():
handle_port_scanning(message)
elif 'data-exfiltration' in alarm_name.lower():
handle_data_exfiltration(message)
return {'statusCode': 200}
def handle_suspicious_traffic(alarm_data):
"""Handle suspicious traffic alerts"""
ec2 = boto3.client('ec2')
# Extract source IP from alarm
source_ip = extract_source_ip_from_alarm(alarm_data)
if source_ip:
# Create NACL rule to block the IP
nacl_id = get_public_subnet_nacl()
# Add deny rule with high priority
ec2.create_network_acl_entry(
NetworkAclId=nacl_id,
RuleNumber=50, # High priority
Protocol='-1', # All protocols
RuleAction='deny',
CidrBlock=f'{source_ip}/32'
)
# Send notification
send_security_notification(
f"Blocked suspicious IP {source_ip} via NACL rule",
alarm_data
)
def handle_port_scanning(alarm_data):
"""Handle port scanning detection"""
# Implement automated blocking and investigation
pass
def handle_data_exfiltration(alarm_data):
"""Handle potential data exfiltration"""
# Implement data loss prevention measures
pass
Conclusion
VPC security architecture requires a comprehensive approach that combines multiple security controls and monitoring capabilities. Key takeaways:
- Layer security controls at network, subnet, and instance levels
- Implement micro-segmentation with security groups and NACLs
- Use VPC endpoints to avoid internet traffic for AWS services
- Monitor traffic patterns with VPC Flow Logs and CloudWatch
- Automate incident response for rapid threat mitigation
Effective VPC security is not just about configuration - it's about creating a living, breathing security ecosystem that adapts to threats and provides comprehensive visibility into your network traffic patterns.
The architecture patterns shown here provide a foundation for enterprise-grade VPC security, but remember that security is an ongoing process that requires continuous monitoring, testing, and improvement.
Comprehensive Network Security Visibility
VPC security architecture is only as effective as your ability to monitor and maintain it. As your environment grows and evolves, you need visibility into how your network security controls are performing and whether they're providing the protection you expect.
While this guide focuses on VPC-level security, remember that network security and IAM security are interconnected. An attacker who gains access to your network through a VPC misconfiguration will likely attempt to escalate privileges through IAM vulnerabilities.
AccessLens provides the IAM visibility that complements your VPC security monitoring:
- Cross-account access analysis that shows how IAM roles can be used to bypass network controls
- Privilege escalation detection that identifies paths attackers might use after gaining network access
- Trust relationship mapping that reveals how network compromise in one account could affect others
- Compliance monitoring that ensures your IAM controls align with your network security policies
Enhance your network security strategy with comprehensive IAM analysis from AccessLens.
Network security and IAM security work together to protect your AWS environment. Don't let gaps in one area undermine the effectiveness of the other.