AWS Security Groups vs NACLs
The two AWS firewall layers. One is stateful and attaches to instances; the other is stateless and attaches to subnets. They layer, they don’t replace each other. Understanding both is the most commonly botched topic for AWS newcomers.
The one-minute summary
| Security Group (SG) | NACL (Network ACL) | |
|---|---|---|
| Scope | Per ENI (instance-level) | Per subnet |
| State | Stateful | Stateless |
| Rules | Allow only (implicit deny) | Allow + Deny, numbered |
| Evaluation | All rules evaluated (permissive union) | First match by rule number |
| Default behaviour | Default SG: allow outbound only | Default NACL: allow all in + out |
| Typical use | Primary filter on everything | Coarse subnet-level guardrails |
| Rules can reference | Other SGs by ID (or CIDR, prefix lists) | Only CIDR blocks |
| How many per resource | Up to 5 SGs per ENI | Exactly 1 NACL per subnet |
Default posture: SGs do almost all the work. NACLs sit permissive unless you have a specific reason to restrict at the subnet level.
Stateful vs stateless — the distinction that matters
Stateful (SG)
When a rule allows an inbound connection, the return traffic is automatically allowed — regardless of outbound rules. When a rule allows an outbound connection, return traffic is likewise automatically permitted.
Inbound rule: allow TCP 443 from 0.0.0.0/0
↓
Client sends SYN to port 443 — allowed
Server sends SYN/ACK back — allowed automatically
Client sends ACK — allowed (tracked connection)
You don’t think about the reverse direction. You configure what should initiate, and the response path follows.
Stateless (NACL)
Every packet is evaluated independently against the rules. Inbound and outbound are checked separately. The NACL doesn’t remember that packet N was part of connection X.
Inbound rule: allow TCP 443 from 0.0.0.0/0
↓
Client SYN to port 443 — allowed
Server SYN/ACK back — ?? depends on OUTBOUND rule on the ephemeral port
Practical consequence: if you use a NACL to filter, you must explicitly allow the reverse direction on the ephemeral port range (1024-65535 typically, or the exact OS range). Forgetting this is the classic “my NACL broke TCP” bug.
Rule evaluation order
SGs
- All rules across all attached SGs are evaluated
- Union of Allows — any Allow matching the traffic → permitted
- Implicit final deny (if no Allow matches)
- No explicit Deny rule type in SGs
NACLs
- Rules are numbered (1–32766); lower number evaluated first
- First match wins — first match stops evaluation
- Explicit Deny exists and is useful (block a specific malicious IP)
- Convention: leave gaps between rule numbers (10, 20, 30 — not 1, 2, 3) so you can insert later
Where they attach
SG → ENI (network interface)
Every EC2 instance has one or more ENIs. Each ENI has up to 5 SGs. The union of their rules filters traffic to and from that ENI. If you stop+start the instance, SGs persist (attached to the ENI, not the instance).
Other services with ENIs (ALB, NLB, RDS, Lambda in VPC, EKS pods via ENIs) also have SGs. The pattern is universal.
NACL → Subnet
Every subnet has exactly one NACL. A new VPC comes with a default NACL that allows all traffic in and out. You can create custom NACLs and associate them with subnets — each subnet always has exactly one association.
The superpower of SGs: referencing other SGs
SGs can allow traffic from another SG instead of from a CIDR:
SG: web-tier
Inbound: HTTPS from 0.0.0.0/0
SG: app-tier
Inbound: TCP 8080 from sg-web-tier ← reference by SG ID
This means: “any instance that has sg-web-tier attached can talk to me on 8080.” It’s dynamic — new web-tier instances auto-apply. It captures identity, not IP, which is robust under auto-scaling.
NACLs can’t do this — they only understand CIDR blocks.
This is the idiomatic way to build tiered architectures in AWS. Web tier → app tier → DB tier, each referenced by SG, no CIDR hardcoding.
When to use NACLs at all
Given SGs do almost everything, why NACLs? Legitimate uses:
- Block a specific bad IP or range — explicit Deny at the subnet edge (SGs can only Allow)
- Coarse subnet isolation — e.g. “DB subnet only accepts traffic from app subnet CIDR” as a coarse control, regardless of SG configuration
- Defense in depth — the second firewall, catching misconfigurations in the first
- Compliance mandates requiring subnet-level controls
For most workloads: default NACLs (allow all) + well-designed SGs are enough.
A typical layering example
A three-tier web app:
Internet
│
▼
┌─────────────────────── Public subnet ───────────────────┐
│ NACL: allow all in/out │
│ │
│ ┌───────────┐ SG: ALB-SG │
│ │ ALB │ Inbound: HTTPS 443 from 0.0.0.0/0 │
│ └─────┬─────┘ Outbound: 8080 to app-sg │
└──────────┼──────────────────────────────────────────────┘
│
┌──────────┼───────── Private subnet ─────────────────────┐
│ NACL: allow in from public-subnet-CIDR │
│ │
│ ┌───────────┐ SG: app-SG │
│ │ EC2 │ Inbound: 8080 from ALB-SG │
│ └─────┬─────┘ Outbound: 5432 to db-sg │
└──────────┼──────────────────────────────────────────────┘
│
┌──────────┼───────── DB subnet ──────────────────────────┐
│ NACL: allow in from private-subnet-CIDR │
│ │
│ ┌───────────┐ SG: db-SG │
│ │ RDS │ Inbound: 5432 from app-SG │
│ └───────────┘ Outbound: (none needed) │
└──────────────────────────────────────────────────────────┘
Notice:
- SGs reference each other by ID (dynamic, identity-based)
- NACLs reference CIDR blocks (coarse)
- Each layer allows only what it needs from the layer above
Common pitfalls
- Stateless NACL bug. You allow 443 inbound, then wonder why nothing works — ephemeral-port return traffic is blocked on outbound. Add outbound allow for
1024-65535. - Wide-open default SG. Newly created VPCs have a default SG allowing all traffic within itself. Don’t use the default SG for anything important; create named ones.
- Rule limits. Hard limits apply (60 inbound + 60 outbound rules per SG; 40 rules per NACL by default). Hit these on busy apps and you need to rethink.
- SG cross-VPC limitations. SGs can reference other SGs only within the same VPC — or across peered VPCs if the ref was set up after peering. Across Transit Gateway, use CIDRs or prefix lists.
- Prefix Lists. A managed list of CIDRs; reference in SG/NACL rules. Use customer-managed prefix lists to avoid repeating CIDR lists across dozens of rules.
- Load balancer SGs. ALB/NLB SGs often confuse newcomers — the target’s SG must allow the ALB’s SG, not the client CIDR.
Mental model for a network engineer
- SG = stateful host firewall that travels with the NIC. Closer to
iptables -m conntrack/firewalld than to a traditional ACL. - NACL = traditional router ACL at the subnet edge. Stateless, numbered rules, allow/deny.
- No L2 concerns — these are L3/L4 constructs; MAC filtering, port security, DHCP snooping don’t apply.