AWS Network ACLs Explained: NACL vs Security Groups
A Network ACL (NACL) is a stateless packet filter that sits at the boundary of an AWS subnet. Every packet entering or leaving a subnet passes through the NACL first, before it ever reaches a security group. NACLs check each packet independently against numbered rules in ascending order, and the first matching rule wins. Unlike security groups, NACLs can explicitly deny traffic.
In practice, most teams rely almost entirely on security groups and leave the default NACL in place. Custom NACLs are mainly useful for blocking specific IP addresses at the subnet level, satisfying compliance requirements for network segmentation, or adding a coarse guardrail above your security groups. If none of those apply right now, you likely do not need to change anything.
This page explains exactly how NACLs work, what makes them different from security groups, when to use them, and the mistakes that break VPC connectivity, including the ephemeral port trap that catches almost everyone at least once.
Simple explanation
Think of a NACL as a checkpoint at the entrance to a neighborhood. Every car (packet) that enters or exits must pass through the checkpoint. The guard checks each car against a numbered list of rules. The moment a car matches a rule, allowed or denied, the guard acts immediately and stops checking. The guard has no memory of previous cars. If a taxi drove in earlier, the guard does not automatically wave the same taxi back out. The outbound lane has its own separate rule list and checks every car again from scratch.
Security groups are the locks on individual front doors. They remember which connections are active and automatically allow the response traffic back through without you writing extra rules. A NACL remembers nothing.
NACLs apply at the subnet level. One NACL covers all resources in a subnet at once. Security groups apply at the resource level. Each EC2 instance, RDS database, or Lambda function in a VPC has its own firewall rules. For most access control decisions, security groups are the right tool. NACLs exist for subnet-wide rules that apply to everything at once, particularly when you need to deny traffic, which security groups cannot do.
Use security groups for all normal day-to-day access control: which ports are open, which services can talk to each other, what the internet can reach.
Use NACLs when you need to explicitly deny traffic at the subnet level, such as blocking a known bad IP, creating hard network boundaries between subnet tiers, or meeting compliance requirements for network-layer controls.
How Network ACLs work
When a packet arrives at a subnet boundary (for example, an HTTP request from the internet headed for an EC2 instance in your VPC), the NACL evaluates it against the inbound rule list first. If the packet is allowed, it continues to the instance’s security group. If the NACL denies it, the packet is dropped immediately and never reaches the security group or the instance.
When the EC2 instance sends a response back, that response packet must pass through the NACL’s outbound rule list. This is the stateless part: the NACL has no memory that this packet is a response to an earlier allowed request. It evaluates the outbound packet as if it were a completely new, unrelated packet.
Inbound path:
Internet -> [NACL inbound rules] -> [Security group inbound rules] -> EC2 instance
Outbound path:
EC2 instance -> [Security group outbound rules] -> [NACL outbound rules] -> InternetThe ephemeral port problem: why it silently breaks things
When a client connects to your server on port 80, the response does not go back to the client on port 80. The client’s operating system picks a temporary (ephemeral) port for its side of the connection, typically somewhere in the range 1024–65535. The server sends the response to that ephemeral port.
Because a NACL has no memory of connections, it evaluates this response packet as a new outbound packet destined for some random port in the 1024–65535 range. If your outbound NACL rules do not allow traffic to that port range, the response is silently dropped. The client gets nothing back. Your security group can be perfectly correct and it will not matter. The NACL has already killed the response before the security group is even consulted.
Client at 1.2.3.4 connects using source port 52341:
Step 1 - Request arrives:
-> Inbound NACL: checks for allow on TCP port 80 from 0.0.0.0/0 [matched rule 100]
-> Security group: checks for allow on TCP port 80 [matched]
-> Request reaches EC2 instance
Step 2 - Response goes back:
-> Security group outbound: stateful, response automatically allowed
-> Outbound NACL: checks for allow on TCP to port 52341 (1.2.3.4)
-> If no rule covers ports 1024-65535: DENIED
-> Client times out even though inbound was fineIf you allow inbound port 80 but forget the outbound ephemeral port rule, every HTTP response is silently dropped at the NACL. The server processes the request. The security group allows the response out. Then the NACL kills it. The client sees a timeout and nothing in your application logs explains why. Fix: add an outbound rule allowing TCP 1024–65535 to 0.0.0.0/0 on any subnet that accepts inbound connections from external clients.
Rule order and first-match behavior
NACL rules have explicit numbers from 1 to 32766, plus a default catch-all rule at *. AWS evaluates them in ascending order, lowest number first. The moment a packet matches a rule, evaluation stops immediately. The remaining rules are never checked. If no numbered rule matches, the * rule applies. In a custom NACL, that default is deny all.
Rule 50: DENY TCP 198.51.100.5/32 ports 0-65535 <- attacker IP blocked first
Rule 100: ALLOW TCP 0.0.0.0/0 port 80 <- everyone else gets through
Rule 110: ALLOW TCP 0.0.0.0/0 port 443
Rule *: DENY ALL <- default, catches everything elseDeny rules must have a lower number than any allow rule covering the same traffic. If rule 100 allows port 80 from anywhere and your deny rule is at number 200, the deny never fires. Rule 100 matched the attacker’s traffic first and evaluation stopped. The deny at 200 is unreachable. To block a specific IP, place the deny rule at a number lower than the general allow.
Leave gaps between rule numbers. Use 100, 200, 300 rather than 1, 2, 3. This lets you insert rules between existing ones without renumbering everything. Once rules are in production, renumbering is error-prone and disruptive.
Default NACL vs custom NACL
Every VPC, whether default or custom, comes with a default NACL. Every subnet is associated with the default NACL unless you explicitly assign a different one. The default NACL allows all inbound and all outbound traffic. It does nothing. It is a pass-through that lets security groups handle access control.
When you create a custom NACL, it starts with a single rule: * DENY ALL. No traffic can pass until you explicitly add allow rules. This is the opposite of the default NACL behavior, which catches many people off guard.
| Default NACL | Custom NACL | |
|---|---|---|
| Starting state | Allows all inbound and outbound | Denies all inbound and outbound |
| Subnet association | All subnets unless reassigned | Only subnets you explicitly assign |
| Best for | Leave alone if you don’t need subnet-level filtering | Blocking IPs, compliance segmentation, subnet guardrails |
Do not associate a new custom NACL with a production subnet until you have added all the rules it needs. A fresh custom NACL denies everything, including load balancer health checks, SSH management traffic, and all established sessions. Associate it before writing your rules and you instantly break the entire subnet. Always write your rules first, review them, then associate.
Network ACLs vs security groups
Both are virtual firewalls, but they work at different layers and in fundamentally different ways. Understanding the difference is the key to debugging most “why is traffic blocked even though my security group is correct?” problems.
| Feature | Network ACL | Security Group |
|---|---|---|
| Scope | Subnet level. Applies to all resources in the subnet. | Resource level. Attached to individual EC2s, RDS instances, etc. |
| Statefulness | Stateless. Each packet evaluated independently, no connection memory. | Stateful. Return traffic automatically allowed, no extra rules needed. |
| Rule processing | Evaluated in number order. Stops at first match. | All rules evaluated together. Most permissive match wins. |
| Explicit deny | Yes. You can write DENY rules. | No. Only ALLOW rules. Everything not allowed is implicitly denied. |
| Default behavior | Default NACL allows all. Custom NACL denies all until rules are added. | Denies all inbound, allows all outbound by default. |
| Rule targets | IP addresses and CIDR ranges only. | IP/CIDR ranges, or references to other security groups by ID. |
| Best use case | Blocking specific IPs, subnet-wide policy, compliance controls. | Day-to-day access control for all resources. The primary firewall layer. |
The most important practical difference: security groups are your primary firewall. NACLs add a secondary layer for the one thing security groups cannot do: explicit denies. Security groups can only allow traffic. If you want to explicitly block traffic from a specific IP, you need a NACL.
When to use Network ACLs
- You need to block a specific IP or CIDR range at the subnet level (security groups cannot deny)
- A compliance framework requires a separate network-layer control distinct from host firewalls
- You want a subnet-wide guardrail that limits damage if a security group gets misconfigured
- Policy requires blocking specific outbound destinations across an entire subnet
Most production VPCs work perfectly with the default NACL. Custom NACLs add genuine value in these specific situations:
Blocking a known malicious IP or CIDR range. Security groups cannot deny; they only allow. If you detect an attack from a specific IP and need to cut it off across an entire subnet immediately, a NACL deny rule is the right tool. It stops the traffic at the subnet boundary before it touches security groups or instances.
Compliance-mandated network segmentation. Some frameworks (PCI-DSS, HIPAA environments) require demonstrable network-layer controls separate from host-level firewalls. A NACL between your web tier and database tier subnets provides that distinct control. See network security best practices for how to layer these correctly.
A coarse safety net above security groups. A NACL that restricts which ports are reachable from the internet on your public subnets creates a subnet-wide guardrail. If someone misconfigures a security group to expose an unexpected port, the NACL limits the blast radius.
Blocking unwanted outbound destinations. If policy requires that no resource in a subnet should reach specific external CIDR ranges, a NACL outbound deny enforces that at the network level. You do not need to rely on every individual resource’s security group being configured correctly.
When not to use Network ACLs
NACLs are the wrong tool for most things teams try to use them for:
Fine-grained per-resource rules. If you want to allow database port 5432 only from your application server, use a security group rule that references the app server’s security group by ID. A NACL works with IP addresses only, applies to the entire subnet, and cannot reference security groups. This makes it far less precise for service-to-service access control inside a VPC.
Service-to-service access control within a VPC. Security groups are much better suited here: stateful, referenceable by ID, and they do not require managing ephemeral port rules. Internal IPs inside a VPC can also change; security group membership does not depend on IP stability.
As a replacement for security groups. Do not attempt to do everything in NACLs. The stateless design means writing rules for both directions of every flow, managing ephemeral port ranges, and debugging becomes significantly more complex. Security groups are simpler for the vast majority of access control needs.
Protecting individual resources differently within the same subnet. If two EC2 instances share a subnet but need different access rules, a NACL cannot help. It applies to both identically. Use security groups, which are per-resource.
Common mistakes
Missing outbound ephemeral port rules. This is the most common NACL misconfiguration. You add an inbound allow rule for port 80 and assume traffic flows. It does not. The response goes back to the client’s ephemeral port (1024–65535), and without an outbound allow for that range, responses are silently dropped. Add an outbound rule for TCP 1024–65535 to
0.0.0.0/0whenever your subnet accepts inbound connections from clients.Placing a deny rule with a higher number than the allow rule for the same traffic. Rule 100 allows port 80 from anywhere, including the attacker. Rule 200 denies the attacker’s IP. The deny never fires because rule 100 matched first and evaluation stopped. Deny rules that target specific sources must have a lower number than the general allow rule for that port.
Associating a blank custom NACL with a live subnet. A new custom NACL has one rule: deny all. If you attach it to a production subnet before adding rules, you instantly break all connectivity. Health checks fail, sessions drop, SSH disappears. Always write your allow rules first, review them, then associate the NACL.
Treating NACLs as stateful. Developers who are used to security groups sometimes add only inbound rules and expect return traffic to flow automatically. It will not. NACLs evaluate every packet independently. You always need explicit outbound rules that cover every type of return traffic your subnet produces.
Troubleshooting a Network ACL that blocks traffic
If traffic is entering a subnet but not reaching resources, or if inbound works but responses never return, work through these steps in order. For a full VPC debugging workflow, see troubleshooting network issues in AWS VPC.
Confirm which NACL is associated with the subnet. Go to VPC > Subnets > select the subnet > Network ACL tab. Confirm it is the NACL you expect. It is easy to have two NACLs and edit the wrong one.
Check both inbound and outbound rules. NACLs are stateless. Both directions need rules. Most people check only inbound and miss the outbound ephemeral port issue entirely.
Check outbound ephemeral ports (1024–65535) specifically. If inbound is correct but clients never see a response, this is the first thing to verify. An outbound allow for TCP 1024–65535 to
0.0.0.0/0needs to be present.Check rule order for deny rules. Look at the rule numbers. If a deny rule has a higher number than an allow rule covering the same traffic, the deny is unreachable. The allow fires first and processing stops.
Temporarily swap back to the default NACL. Associate the default NACL (allow all) with the subnet and test. If traffic works, the problem is in your custom NACL rules. This is the fastest way to isolate NACL issues from route table or security group problems.
Use VPC Flow Logs to see what is being accepted or rejected. Flow logs record NACL accept and reject decisions per packet. If you see REJECT entries for traffic that should be allowed, you have found the rule gap. Look at both the source and destination port in the rejected flow.
Check the address range if using private subnets. If traffic is coming from resources in private IP address ranges, confirm your NACL rules cover those CIDR blocks and not just public internet ranges.
Example: blocking an IP at the subnet level
This example creates a NACL for a public subnet that blocks a specific attacker IP while allowing normal HTTP and HTTPS traffic. It includes the outbound ephemeral port rule. Without it, all HTTP and HTTPS responses would be silently dropped.
# Create a custom NACL for the public subnet
aws ec2 create-network-acl \
--vpc-id vpc-0123456789abcdef0 \
--tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=public-subnet-nacl}]'
# INBOUND: Block attacker IP first (rule 50, evaluated before rule 100)
aws ec2 create-network-acl-entry \
--network-acl-id acl-0123456789abcdef0 \
--rule-number 50 \
--protocol tcp \
--rule-action deny \
--ingress \
--cidr-block 198.51.100.5/32 \
--port-range From=0,To=65535
# INBOUND: Allow HTTP from everyone else
aws ec2 create-network-acl-entry \
--network-acl-id acl-0123456789abcdef0 \
--rule-number 100 \
--protocol tcp \
--rule-action allow \
--ingress \
--cidr-block 0.0.0.0/0 \
--port-range From=80,To=80
# INBOUND: Allow HTTPS from everyone else
aws ec2 create-network-acl-entry \
--network-acl-id acl-0123456789abcdef0 \
--rule-number 110 \
--protocol tcp \
--rule-action allow \
--ingress \
--cidr-block 0.0.0.0/0 \
--port-range From=443,To=443
# OUTBOUND: Allow response traffic on ephemeral ports (required, NACL is stateless)
# Without this rule, responses to every inbound HTTP/HTTPS request are dropped
aws ec2 create-network-acl-entry \
--network-acl-id acl-0123456789abcdef0 \
--rule-number 100 \
--protocol tcp \
--rule-action allow \
--egress \
--cidr-block 0.0.0.0/0 \
--port-range From=1024,To=65535
# Associate the NACL with the subnet (only after rules are in place)
aws ec2 replace-network-acl-association \
--association-id aclassoc-0123456789abcdef0 \
--network-acl-id acl-0123456789abcdef0Why the deny rule must be at number 50: the allow rules at 100 and 110 match traffic from any IP, including the attacker’s. If the deny were at number 200, the attacker’s request would match rule 100 first, be allowed, and the deny would never run. The attacker’s IP must be denied before the general allow rules evaluate.
Why the outbound ephemeral port rule is required: when a client connects to port 80, the server sends its response back to the client’s randomly chosen source port, somewhere in 1024–65535. The NACL evaluates this response as a brand new outbound packet. Without rule 100 on the egress side, every HTTP and HTTPS response is silently dropped at the NACL. The instance processes the request, the security group allows the response out, and then the NACL kills it. The client sees a timeout.
Summary
- NACLs are stateless subnet-level packet filters. Every packet is evaluated independently with no memory of connection state.
- Rules are numbered and evaluated in ascending order. The first matching rule wins and evaluation stops immediately.
- NACLs support explicit deny rules, the one capability that security groups do not have.
- Because NACLs are stateless, you must allow both inbound traffic and outbound ephemeral port responses (1024–65535) or client responses will be silently dropped.
- The default NACL allows all traffic. Custom NACLs deny all traffic until you add allow rules.
- One NACL can cover multiple subnets; each subnet can only have one NACL at a time.
- Most teams leave the default NACL alone and rely on security groups. Add custom NACLs only when you need subnet-level IP blocking, compliance-mandated network segmentation, or a coarse guardrail above your security groups.
Frequently asked questions
Do I need custom NACLs in most AWS environments?
No. The default NACL allows all traffic, and most teams rely entirely on security groups for access control. Custom NACLs add value when you need to block specific IP addresses at the subnet level, enforce compliance-required network segmentation, or add a coarse safety net on top of security groups. If none of those apply, the default NACL is fine.
Why does traffic fail even when my security group allows it?
Both the NACL and the security group must allow traffic for a connection to succeed. Traffic hits the NACL first at the subnet boundary. If the NACL blocks it, the packet never reaches your resource. The security group never gets a chance to evaluate it. The other common cause: NACLs are stateless, so if your outbound NACL does not allow ephemeral ports (1024-65535), response traffic is silently dropped even when inbound was allowed.
Can one NACL be associated with multiple subnets?
Yes. A single NACL can be associated with as many subnets as you want within the same VPC. This is common when you want consistent filtering across all your public subnets or all your private subnets.
Can a subnet have more than one NACL?
No. Each subnet can be associated with exactly one NACL at a time. If you associate a new NACL with a subnet, the previous association is replaced automatically.
What is the easiest way to decide between a security group and a NACL?
Default to security groups for everything. Only reach for a NACL when you need to explicitly deny traffic, such as blocking a specific IP or CIDR range at the subnet level, or when a compliance requirement calls for a separate network-layer control. Security groups handle the vast majority of access control needs without the complexity of managing stateless rules.