AWS Security Groups vs NACLs: Stateful vs Stateless, and When Each One Matters

Quick answer
Security Groups and Network ACLs both filter traffic in a VPC, but they operate at different layers, evaluate rules differently, and fail in different ways. Here's how I decide which one to reach for — and the ephemeral port mistake that breaks NACLs for almost everyone.
- The Core Difference: Stateful vs Stateless
- Scope: ENI vs Subnet
- Rule Evaluation: All Rules vs First Match
- The Ephemeral Port Problem
- Defaults Are Opposites
10 min read · AWS
Every AWS networking interview asks this question, and every VPC troubleshooting session eventually runs into it: what's the difference between a Security Group and a Network ACL? Both filter traffic. Both live in your VPC. Both show up in the console under "Security." And yet they behave so differently that confusing them is one of the most common causes of "it works from here but not from there" connectivity bugs.
I'll give you the practical version first: Security Groups do 95% of the work in a well-designed VPC. NACLs are a blunt instrument you reach for in a handful of specific situations. If you're allowing traffic with NACLs and blocking it with Security Groups, you're using both tools the way they were designed. If you're trying to build fine-grained policy in NACLs, you're going to have a bad time.
Now the details, because the details are where the outages live.
The Core Difference: Stateful vs Stateless
Security Groups are stateful. If you allow inbound traffic on port 443, the response traffic is automatically allowed back out — regardless of your outbound rules. AWS tracks the connection and matches return packets to it. You never write a rule for response traffic.
NACLs are stateless. Every packet is evaluated against the rule list in isolation. If you allow inbound 443, the response — which leaves on an ephemeral port — must be explicitly allowed by an outbound rule. No connection tracking, no memory, no help.
This single difference explains most of the operational pain people have with NACLs. A Security Group rule says "allow this conversation." A NACL rule says "allow packets that look like this, in this direction" — and a TCP conversation involves packets that look different in each direction.
Scope: ENI vs Subnet
Security Groups attach to Elastic Network Interfaces — which means they protect individual resources: EC2 instances, RDS databases, Lambda functions in a VPC, EKS pods (with security groups for pods), load balancer nodes. A resource can have up to five security groups, and the rules are aggregated.
NACLs attach to subnets. Every resource in the subnet is subject to the same NACL, and a subnet has exactly one. Traffic between two instances in the same subnet never touches the NACL — it only evaluates traffic crossing the subnet boundary.
That last point trips people up during incident response: if two instances in the same subnet can't talk, the NACL is innocent. Look at the Security Groups.
Rule Evaluation: All Rules vs First Match
Security Groups evaluate all rules before deciding. There's no ordering, no priority, and only allow rules exist. If any rule allows the traffic, it's allowed. You cannot write a deny rule in a Security Group — the implicit default is "deny everything not explicitly allowed."
NACLs evaluate rules in number order, lowest first, and stop at the first match. Each rule is an explicit allow or deny. Rule 100 allowing 0.0.0.0/0 on port 443 beats rule 200 denying a specific IP — because rule 100 matched first. If you want to block an IP and allow everyone else, the deny rule must have a lower number than the allow rule:
Rule # Type Protocol Port Source Allow/Deny
90 HTTPS TCP 443 203.0.113.66/32 DENY
100 HTTPS TCP 443 0.0.0.0/0 ALLOW
* ALL ALL ALL 0.0.0.0/0 DENY
This is the one genuinely useful thing NACLs can do that Security Groups can't: explicit deny. More on that below.
The Ephemeral Port Problem
Here's the mistake I've seen take down production: someone tightens a NACL to "only allow ports 80 and 443" — inbound and outbound. Inbound HTTPS still works for a moment in testing... and then everything breaks.
When a client connects to your server on port 443, the response goes back to the client's ephemeral port — a high-numbered port chosen by the client's OS. Linux typically uses 32768–60999, AWS NAT Gateways and ELBs use 1024–65535, Windows uses 49152–65535. Your NACL's outbound rules must allow that range or the responses get dropped:
# Outbound rules for a subnet serving HTTPS
Rule # Type Protocol Port range Destination Allow/Deny
100 Custom TCP TCP 1024-65535 0.0.0.0/0 ALLOW
And it cuts both ways: if instances in the subnet make outbound calls (package repos, external APIs, AWS APIs), the inbound rules must allow the ephemeral range for the responses coming back.
By the time you've allowed ephemeral ranges in both directions, your "locked down" NACL allows most TCP traffic anyway. This is why fine-grained NACL policies are mostly security theater — the stateless model forces you to open wide port ranges that a stateful Security Group never needs.
AWS Cost & Architecture Review Checklist
The questions we ask in a paid AWS review — rightsizing, storage classes, network egress, and the usual five-figure surprises. Plain Markdown.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
Defaults Are Opposites
Another asymmetry worth memorizing:
- The default NACL that comes with your VPC allows all inbound and outbound traffic. A custom NACL you create starts by denying everything.
- A default Security Group allows all traffic from itself (resources in the same group can talk) and all outbound. A custom Security Group starts with no inbound rules (deny all in) and allows all outbound.
So creating a custom NACL and forgetting to add rules silently blackholes a subnet, while creating a custom Security Group fails safe in the direction you'd expect. Guess which one generates the 2 a.m. page.
Security Group Referencing: The Killer Feature
The reason Security Groups win for day-to-day policy isn't just statefulness — it's that rules can reference other Security Groups instead of CIDR blocks:
# RDS security group: allow Postgres only from the app tier
Inbound: TCP 5432, source = sg-0abc123 (app-tier-sg)
Now it doesn't matter what IP the app instances have, how many there are, or whether they get replaced by an autoscaling event. Membership in the group is the policy. NACLs can't do this — they only understand CIDR ranges, which means subnet-level IP math and rules that rot as your architecture evolves.
In EKS, this same model extends to pods via security groups for pods on supported instance types — your database policy can target a set of pods rather than a node CIDR. I covered the VPC side of that in EKS networking with the VPC CNI.
Quick Comparison
| Security Group | Network ACL | |
|---|---|---|
| Operates at | ENI (resource) level | Subnet level |
| State | Stateful — return traffic auto-allowed | Stateless — return traffic needs explicit rules |
| Rule types | Allow only | Allow and deny |
| Evaluation | All rules evaluated | Number order, first match wins |
| Default (custom) | Deny all inbound, allow all outbound | Deny everything |
| Can reference SGs | Yes | No — CIDR only |
| Applies to intra-subnet traffic | Yes | No |
| Per-resource limit | 5 per ENI, 60 rules each (soft) | 1 per subnet, 20 rules (soft, max 40) |
When I Actually Use NACLs
After years of building VPCs, my NACL use cases come down to three:
1. Blocking known-bad IPs or ranges. Security Groups can't deny. If a specific IP is hammering your public subnet and you want it gone at the network layer without touching application config, a low-numbered NACL deny rule is the fastest tool. (For anything beyond a handful of IPs, use AWS WAF or a firewall — NACL rule limits are tiny.)
2. Subnet-level guardrails. A coarse backstop that says "database subnets never accept traffic from the public subnets, period" protects you from a fat-fingered Security Group rule later. The NACL isn't the policy — it's the failsafe under the policy.
3. Compliance checkboxes. Some frameworks explicitly require subnet-level filtering as a separate control layer. Fine. Keep the rules coarse, document the ephemeral port ranges, and move on.
Everything else — service-to-service policy, tier isolation, least privilege — belongs in Security Groups, where statefulness and SG-referencing make the rules both tighter and easier to maintain.
Troubleshooting: Which One Is Dropping My Packets?
When connectivity breaks and you suspect filtering:
- Check whether the resources share a subnet. Same subnet → NACL is irrelevant, it's Security Groups (or the OS firewall).
- Use VPC Reachability Analyzer. It evaluates Security Groups, NACLs, and route tables along the path and tells you exactly which component blocked the flow. This tool has saved me hours of rule-by-rule auditing.
- Read VPC Flow Logs. A
REJECTrecord tells you traffic was dropped, and the direction plus port pattern usually identifies the culprit: requests arriving but responses dying is the classic stateless-NACL-ephemeral-port signature. - Remember asymmetry. With NACLs, test both directions. A flow needs four green lights: inbound NACL, inbound SG, outbound SG, outbound NACL — and the return path re-runs the NACL checks with the ports swapped.
The Bottom Line
Security Groups are your policy layer: stateful, resource-scoped, composable through group references. NACLs are your guardrail layer: stateless, subnet-scoped, capable of explicit deny but too coarse for real policy. Use Security Groups for everything you can, NACLs for the few things only they can do, and never try to build least-privilege networking out of stateless rules — the ephemeral port problem guarantees you'll either break traffic or open the ranges so wide the rules stop meaning anything.
Frequently Asked Questions
Can a Security Group block a specific IP address?
No. Security Groups only support allow rules — there is no deny. If a rule allows 0.0.0.0/0 on a port, you can't carve out an exception for one bad actor. To block a specific IP you need a NACL deny rule (with a lower rule number than your allow rules), AWS WAF for HTTP traffic, or a network firewall.
Do Security Groups and NACLs both apply to the same traffic?
Yes, when traffic crosses a subnet boundary it must pass both: the NACL on the subnet and the Security Group on the ENI. Either one can drop it, so a flow needs the inbound NACL, inbound SG, and on the way back the outbound SG and outbound NACL to all permit it. Traffic between instances in the same subnet skips the NACL entirely.
Why does my NACL break responses even though inbound rules look correct?
Because NACLs are stateless. Response packets leave on ephemeral ports (typically 1024–65535), so your outbound rules must allow that range to the client's address. A NACL that only allows outbound 80/443 silently drops every response to an inbound request — the classic symptom is connections that hang after the SYN.
Should I use NACLs at all if Security Groups are stateful?
Use them sparingly. Keep the default NACL (allow all) or a coarse subnet-level guardrail, and put your real policy in Security Groups. The cases where NACLs earn their keep: blocking specific IPs, enforcing "database subnets never talk to public subnets" as a backstop, and compliance frameworks that mandate subnet-level filtering as a separate control.
If you're designing the VPC layout these controls live in, I've written about VPC design for EKS and the fix for unreachable EC2 instances — which is, more often than you'd think, one of these two layers misconfigured.
Need help auditing or designing your AWS network security? Get in touch — I do this for a living.
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


