Security Groups / NACLs
Security Groups are stateful virtual firewalls attached to ENIs (EC2, RDS, Lambda VPC, load balancers). Default-deny inbound, default-allow outbound, and rules reference other security groups so “web -> db” is one rule instead of an ever-changing IP list.
Stateful, references, deny, audit
EXAMPLE
// 1) Anatomy
// • Inbound rules: default DENY ALL — explicitly allow what you need
// • Outbound rules: default ALLOW ALL — restrict for tight environments
// • Stateful: a response to an allowed inbound packet is automatically allowed outbound
// • Multiple groups can be attached to one ENI; rules UNION
// 2) A typical web tier
awscli> aws ec2 create-security-group \\
--group-name web-sg --description 'web tier' --vpc-id vpc-123
# returns groupId like sg-aaaa
// Allow HTTP/HTTPS from anywhere (ALB)
awscli> aws ec2 authorize-security-group-ingress --group-id sg-aaaa \\
--ip-permissions IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges='[{CidrIp=0.0.0.0/0}]' \\
IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges='[{CidrIp=0.0.0.0/0}]'
// 3) The database tier — reference the WEB SG, not IPs
awscli> aws ec2 create-security-group \\
--group-name db-sg --description 'db tier' --vpc-id vpc-123
# returns sg-bbbb
awscli> aws ec2 authorize-security-group-ingress --group-id sg-bbbb \\
--ip-permissions IpProtocol=tcp,FromPort=5432,ToPort=5432,UserIdGroupPairs='[{GroupId=sg-aaaa}]'
// Now any instance in sg-aaaa can talk to Postgres on sg-bbbb — no IP list to maintain.
// Add a new EC2 instance to sg-aaaa and it automatically gets DB access.
// 4) Terraform — declarative groups
resource 'aws_security_group' 'web' {
name = 'web-sg'
description = 'web tier'
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = 'tcp'
cidr_blocks = ['0.0.0.0/0']
}
ingress {
from_port = 443
to_port = 443
protocol = 'tcp'
cidr_blocks = ['0.0.0.0/0']
}
egress {
from_port = 0
to_port = 0
protocol = '-1'
cidr_blocks = ['0.0.0.0/0'] # default — restrict in tight envs
}
}
resource 'aws_security_group' 'db' {
name = 'db-sg'
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = 'tcp'
security_groups = [aws_security_group.web.id]
}
}
// 5) Outbound restriction (zero-trust)
# By default outbound is open. For sensitive services, lock it down:
resource 'aws_security_group' 'app' {
egress {
from_port = 443
to_port = 443
protocol = 'tcp'
cidr_blocks = ['0.0.0.0/0'] # only HTTPS to internet
}
egress {
from_port = 5432
to_port = 5432
protocol = 'tcp'
security_groups = [aws_security_group.db.id]
}
}
// 6) NACLs vs Security Groups
// • Security Groups — STATEFUL; attached to ENI; default deny in
// • NACLs — STATELESS; attached to subnet; rules are numbered; default allow
// Use SGs for app-level allowlisting; NACLs for subnet-level guardrails (block specific IPs, RFC1918 exceptions).
// 7) Audit + visualisation
awscli> aws ec2 describe-security-groups --group-ids sg-aaaa
awscli> aws ec2 describe-network-interfaces --filters 'Name=group-id,Values=sg-aaaa'
# Reachability Analyzer — does sg-A's EC2 actually reach sg-B's RDS?
awscli> aws ec2 create-network-insights-path \\
--source eni-aaa --destination eni-bbb --protocol tcp --destination-port 5432
awscli> aws ec2 start-network-insights-analysis --network-insights-path-id nip-123
awscli> aws ec2 describe-network-insights-analyses --network-insights-analysis-ids nia-123
# Visualise: VPC > Network Analysis > Reachability Analyzer
// 8) Common patterns
// • ALB SG — allow 80/443 from internet; allow ALL to app SG
// • App SG — allow 8080 from ALB SG; restrict outbound
// • DB SG — allow 5432 from App SG ONLY
// • Bastion SG — allow 22 from corporate IP range only; allow to App SG on 22
// • Lambda SG — outbound to DB SG; needed for VPC Lambdas
// 9) Cross-account / cross-VPC references
// • Reference an SG in a peered VPC by its full SG id
// • For Transit Gateway / VPC peering, SGs don't traverse — use prefix lists or IP CIDRs
// 10) Prefix Lists — managed IP collections
# Use to grant access to a list of IPs that may change (CDN, GitHub Actions, etc.):
awscli> aws ec2 create-managed-prefix-list --prefix-list-name 'cdn-ips' --max-entries 30 \\
--address-family IPv4 --entries Cidr=151.101.0.0/16
# In SG rule:
--source-prefix-list pl-123
# AWS-managed prefix lists exist for S3, DynamoDB endpoints — use them in VPC endpoint policies.
// 11) Limits
# • Up to 60 inbound + 60 outbound rules per SG (raise to 1000 via support)
# • Up to 5 SGs per ENI (raise to 16)
# • Up to 2,500 SGs per VPC
# If you hit limits, consolidate or refactor — usually a sign of over-fragmented rules.
// 12) Cleanup hygiene
# • Quarterly review — list SGs, find unused (no ENIs attached)
# • Tag SGs with Owner + Purpose
# • Remove '0.0.0.0/0' on SSH / RDP — use SSM Session Manager or bastion + IP restriction
# • Watch for 'any' rules: 0.0.0.0/0 + protocol -1 + all ports = open door
// 13) Common bugs
// • SG attached to instance but no ENI rule path — verify by tracing ENI through subnet route table
// • 0.0.0.0/0 on database ports — common mistake; lock to app SG only
// • Forgot to allow outbound 443 from VPC Lambdas calling external APIs — VPC Lambdas don't get internet for free
// • Reaching cross-VPC over SG references — only works in peered VPCs
// • Adding a rule and waiting 'a few minutes' for it to take effect — SG changes are nearly instant
// • Treating SGs as static IP lists — use SG-to-SG references; auto-scaling makes IPs ephemeral
// • NACL deny overriding SG allow — NACL evaluated first; check both
Why it matters
Security Groups are stateful and reference each other — let the “web SG can talk to the DB SG” rule replace IP lists that go stale every deploy. Default-deny inbound, restrict outbound on sensitive tiers, use Reachability Analyzer to verify before you ship, and prune unused groups quarterly so the inventory stays auditable.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# Security Groups — stateful, allow-only. # NACLs — stateless, evaluated at the subnet boundary.Try it Yourself »
Discussion
Loading…