Network ACLs in Depth
A Network ACL (Access Control List, often shortened to NACL and pronounced “nackle”) is a firewall that sits at the edge of a subnet inside your VPC (Virtual Private Cloud, your own isolated network in AWS). Every packet entering or leaving the subnet is checked against the NACL’s rules. NACLs are a coarse, subnet-wide guardrail — they protect groups of resources at once, rather than individual servers. Understanding how their rules are evaluated, and the fact that they are stateless, is the difference between a NACL that quietly works and one that silently drops half your traffic.
What a NACL controls
A NACL is attached to one or more subnets. Each subnet must have exactly one NACL at a time, but a single NACL can be shared by many subnets. The NACL filters traffic at the subnet boundary: traffic moving into the subnet hits the inbound rules, and traffic moving out hits the outbound rules.
This makes NACLs different from security groups, which attach to individual resources (specifically to an ENI, an Elastic Network Interface — the virtual network card on an instance). Think of the NACL as the fence around a whole neighborhood, and the security group as the lock on each front door.
How NACL rules are evaluated
Every NACL rule has a rule number. AWS evaluates rules from the lowest number to the highest, and the first rule that matches the traffic wins — evaluation stops immediately. Lower numbers therefore have higher priority.
The evaluation works like this:
- A packet arrives at the subnet boundary.
- AWS checks it against the inbound rules in ascending rule-number order.
- The first rule whose protocol, port range, and CIDR (Classless Inter-Domain Routing, a notation like
10.0.0.0/16that describes a range of IP addresses) match the packet decides the outcome —ALLOWorDENY. - If no rule matches, the packet hits the default deny at the very end and is dropped.
Every NACL has an unremovable final rule shown with a * for its rule number. It always denies anything not matched by an earlier rule. You cannot edit or delete it.
Tip: Leave gaps between your rule numbers (10, 20, 30 rather than 1, 2, 3). This lets you insert a new rule between two existing ones later without renumbering everything.
NACLs are stateless — the big gotcha
This is the single most important thing to understand. Security groups are stateful: if you allow an inbound request, the response is automatically allowed back out, no matter the outbound rules. NACLs are stateless: they have no memory of connections. Each direction is judged entirely on its own.
That means every connection needs a matching rule in BOTH directions. When a client connects to your web server on port 443, the server’s reply does not come back on port 443 — it comes back on an ephemeral port (a temporary, high-numbered port the operating system picks for the return leg of the conversation). So your outbound rules must allow that ephemeral port range, or the response is dropped and the connection appears to hang.
Common ephemeral port ranges:
| Client operating system | Ephemeral port range |
|---|---|
| Modern Linux kernels | 32768–60999 |
| Windows Server 2008+ / 10+ | 49152–65535 |
| AWS NAT Gateway | 1024–65535 |
| Safe catch-all to use in NACLs | 1024–65535 |
Because clients run all sorts of operating systems, the practical advice is to allow 1024-65535 for outbound responses on the inbound-facing subnet.
The default NACL
When you create a VPC, AWS creates a default NACL that allows all inbound and outbound traffic. Any subnet you create is automatically associated with this default NACL until you change it. So out of the box, NACLs do nothing to filter you — that is intentional, because security groups are meant to be your primary firewall.
A custom NACL, by contrast, starts with only the default-deny rule and blocks everything until you add explicit allow rules.
Adding rules — console
To allow inbound HTTPS (port 443) into a subnet, and the matching ephemeral return traffic outbound:
- Open the VPC console and choose Network ACLs in the left menu.
- Select your NACL (for example the one associated with
subnet-0a1b2c3d). - Open the Inbound rules tab and choose Edit inbound rules.
- Choose Add new rule: set Rule number
100, TypeHTTPS (443), Source0.0.0.0/0, Allow. Save. - Open the Outbound rules tab and choose Edit outbound rules.
- Choose Add new rule: set Rule number
100, TypeCustom TCP, Port range1024-65535, Destination0.0.0.0/0, Allow. Save.
Adding rules — AWS CLI
The same two rules with AWS CLI v2. The inbound HTTPS allow:
aws ec2 create-network-acl-entry \
--network-acl-id acl-0a1b2c3d \
--rule-number 100 \
--protocol tcp \
--port-range From=443,To=443 \
--cidr-block 0.0.0.0/0 \
--ingress \
--rule-action allow
The matching outbound ephemeral-port allow (--egress instead of --ingress):
aws ec2 create-network-acl-entry \
--network-acl-id acl-0a1b2c3d \
--rule-number 100 \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 0.0.0.0/0 \
--egress \
--rule-action allow
To confirm the rules landed, describe the NACL:
aws ec2 describe-network-acls --network-acl-ids acl-0a1b2c3d \
--query "NetworkAcls[0].Entries"
Output:
[
{ "RuleNumber": 100, "Protocol": "6", "RuleAction": "allow", "Egress": false, "CidrBlock": "0.0.0.0/0", "PortRange": { "From": 443, "To": 443 } },
{ "RuleNumber": 32767, "Protocol": "-1", "RuleAction": "deny", "Egress": false, "CidrBlock": "0.0.0.0/0" },
{ "RuleNumber": 100, "Protocol": "6", "RuleAction": "allow", "Egress": true, "CidrBlock": "0.0.0.0/0", "PortRange": { "From": 1024, "To": 65535 } },
{ "RuleNumber": 32767, "Protocol": "-1", "RuleAction": "deny", "Egress": true, "CidrBlock": "0.0.0.0/0" }
]
The 32767 rules with action deny are the unremovable default-deny entries (the console shows them as *). Protocol 6 is TCP; -1 means all protocols.
When to use a NACL (and when not to)
Use a NACL when you need a broad, subnet-wide rule that applies regardless of which security group an instance uses — for example, blocking a known-malicious IP range across an entire subnet, or enforcing that a private database subnet can never talk to the public internet. Because NACLs support explicit DENY, they can do something security groups cannot: security groups only allow, never deny.
Do not use a NACL as your primary firewall. Their statelessness makes them fiddly and easy to misconfigure, and they cannot reference security groups or be scoped to a single instance. Reach for security groups first for per-resource access control, and layer a NACL on top only as a coarse subnet guardrail.
Warning: A misordered NACL rule is a classic outage cause. If you add a low-numbered
DENYrule that is broader than you intended, it wins over every higher-numberedALLOW, and traffic stops. Always double-check rule numbers after editing.
There is no charge for using NACLs — they are a free VPC feature. The only cost-adjacent concern is debugging time, which is why getting the stateless return rules right the first time matters.
Best practices
- Keep the default NACL permissive and rely on security groups for the bulk of your access control; introduce custom NACLs only for genuine subnet-wide rules.
- Always add matching inbound and outbound rules — and remember the ephemeral port range (
1024-65535) for return traffic. - Number rules in increments of 10 or 100 so you can insert rules later without renumbering.
- Put broad
DENYrules (such as blocking a bad IP block) at low numbers so they take precedence; put generalALLOWrules at higher numbers. - Document why each rule exists — NACLs are shared across subnets and a confusing rule can break unrelated workloads.
- Combine NACLs with VPC Flow Logs so you can see exactly which packets a rule is dropping when something breaks.