Security Groups vs NACLs on AWS
February 13, 2026
|
AWS
Security
Networking
Stateful vs stateless, defense in depth.
Security Groups vs NACLs: Defense in Depth
AWS provides two layers of network firewall: Security Groups (instance-level, stateful) and Network ACLs (subnet-level, stateless). Understanding when to use each — and how they complement each other — is essential for a robust security posture.
Security Groups: Stateful Firewalls
Security Groups operate at the ENI (Elastic Network Interface) level:
- Stateful — If you allow inbound traffic, the response is automatically allowed outbound
- Allow rules only — You cannot create deny rules; anything not explicitly allowed is denied
- Evaluate all rules — All rules are evaluated before deciding (no ordering)
- Instance-level — Applied directly to EC2 instances, RDS, Lambda, etc.
# Web server Security Group
aws ec2 create-security-group --group-name web-sg --description "Web servers"
aws ec2 authorize-security-group-ingress --group-id sg-xxx \
--protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-xxx \
--protocol tcp --port 80 --cidr 0.0.0.0/0
# Database Security Group - only allow from web-sg
aws ec2 authorize-security-group-ingress --group-id sg-db \
--protocol tcp --port 5432 --source-group sg-xxxNACLs: Stateless Firewalls
Network ACLs operate at the subnet level:
- Stateless — Inbound and outbound rules are evaluated independently; you must allow both directions
- Allow and deny rules — You can explicitly deny specific traffic
- Ordered evaluation — Rules are processed in order by rule number (lowest first)
- Subnet-level — Applied to all resources in the subnet
# NACL for data subnet - allow PostgreSQL from private subnets only
aws ec2 create-network-acl-entry --network-acl-id acl-xxx \
--rule-number 100 --protocol tcp --port-range From=5432,To=5432 \
--cidr-block 10.0.10.0/24 --rule-action allow --ingress
# Deny all other inbound
aws ec2 create-network-acl-entry --network-acl-id acl-xxx \
--rule-number 200 --protocol -1 \
--cidr-block 0.0.0.0/0 --rule-action deny --ingress
# Allow return traffic (stateless - must be explicit)
aws ec2 create-network-acl-entry --network-acl-id acl-xxx \
--rule-number 100 --protocol tcp --port-range From=1024,To=65535 \
--cidr-block 10.0.10.0/24 --rule-action allow --egressSide-by-Side Comparison
| Feature | Security Groups | NACLs |
|---|---|---|
| Level | Instance (ENI) | Subnet |
| Statefulness | Stateful | Stateless |
| Rules | Allow only | Allow and Deny |
| Evaluation | All rules | Ordered (by number) |
| Default | Deny all inbound | Allow all |
| Scope | Per-resource | Per-subnet |
Defense in Depth Strategy
- Security Groups (primary control) — Fine-grained, per-resource rules. This is where 90% of your firewall logic lives.
- NACLs (secondary control) — Broad subnet-level rules for additional protection:
- Block known malicious IP ranges
- Restrict data subnets to private subnet CIDR only
- Add explicit deny rules that Security Groups can't provide
Best Practices
- Reference Security Groups by ID — Use source Security Group references instead of IP ranges for inter-tier communication
- Keep NACLs simple — Only add rules that Security Groups can't handle (explicit denies)
- Name everything — Use descriptive names and tags for both SGs and NACLs
- Audit regularly — Use AWS Config rules to detect overly permissive Security Groups
- Avoid 0.0.0.0/0 on database ports — This is the #1 security misconfiguration in AWS
Eazy SaaS Tip: We use Security Groups as the primary firewall with NACLs providing a safety net. Our standard VPC template includes locked-down NACLs on data subnets that only allow traffic from application subnets — even if someone misconfigures a Security Group, the NACL provides protection.