Network ACLs
Subnet-level security, allow/deny rules, stateless filtering, default deny-all, and rule ordering.
Network ACLs (NACLs) provide subnet-level firewall control. Unlike security groups (resource-level), NACLs apply to all traffic entering or leaving a subnet. They are stateless: you must explicitly allow both inbound and outbound traffic.
NACLs vs Security Groups
| Feature | Network ACL | Security Group |
|---|---|---|
| Level | Subnet | Resource |
| State | Stateless | Stateful |
| Rules | Allow and deny | Allow only |
| Order | Evaluated by rule number | All rules combined |
| Default | Deny all | Varies by direction |
Use both for defense in depth: NACLs as a subnet-level barrier, security groups for fine-grained resource control.
Stateless Filtering
NACLs are stateless. If you allow inbound traffic on port 443, you must also allow outbound traffic on the ephemeral ports (1024–65535) for the response, or the return traffic will be blocked.
Client ──► Inbound Rule (443) ──► Subnet ──► Resource
│
Client ◄── Outbound Rule (ephemeral) ◄──┘ (response)
Rule of thumb: For each allowed inbound port, add an outbound rule for ephemeral ports. For each allowed outbound port, add an inbound rule for ephemeral ports.
Default Deny-All
When you create a subnet, it gets a default NACL with:
- Inbound: Deny all
- Outbound: Deny all
You must add explicit allow rules for traffic to flow. A final implicit rule denies everything not matched.
Rule Ordering
Rules are evaluated in ascending order by rule number. The first matching rule wins. Use gaps (e.g., 100, 200, 300) to insert rules later without renumbering.
Example Rule Order
| Rule # | Type | Protocol | Port | Source/Dest | Action |
|---|---|---|---|---|---|
| 100 | Inbound | TCP | 443 | 0.0.0.0/0 | Allow |
| 110 | Inbound | TCP | 22 | 10.0.0.0/8 | Allow |
| 200 | Inbound | TCP | 1024-65535 | 0.0.0.0/0 | Allow |
| 32767 | Inbound | * | * | 0.0.0.0/0 | Deny |
The 32767 rule is the implicit deny-all. Custom deny rules should have lower numbers if you want to explicitly block certain traffic before the implicit deny.
Allow and Deny Rules
Allow Rules
Permit traffic that matches the rule. Use for normal access patterns.
{
"rule_number": 100,
"direction": "inbound",
"protocol": "tcp",
"port_range": "443",
"cidr": "0.0.0.0/0",
"action": "allow"
}
Deny Rules
Explicitly block traffic. Use to block known bad actors or restrict access.
{
"rule_number": 50,
"direction": "inbound",
"protocol": "tcp",
"port_range": "22",
"cidr": "203.0.113.0/24",
"action": "deny"
}
Place deny rules with lower rule numbers so they’re evaluated before allow rules.
Creating a Network ACL
Via API
curl -X POST https://api.yourdomain.com/v1/network-acls \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "private-subnet-acl",
"vpc_id": "vpc_abc123",
"description": "NACL for private application subnets"
}'
Response:
{
"id": "acl_xyz789",
"name": "private-subnet-acl",
"vpc_id": "vpc_abc123",
"inbound_rules": [],
"outbound_rules": [],
"created_at": "2026-03-01T12:00:00Z"
}
Adding Rules
curl -X POST https://api.yourdomain.com/v1/network-acls/acl_xyz789/rules \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rule_number": 100,
"direction": "inbound",
"protocol": "tcp",
"port_range": "443",
"cidr": "0.0.0.0/0",
"action": "allow"
}'
Associating with a Subnet
curl -X PATCH https://api.yourdomain.com/v1/subnets/subnet_priv_1 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"network_acl_id": "acl_xyz789"}'
Common NACL Configurations
Public Subnet (Web Servers)
{
"inbound": [
{ "rule_number": 100, "protocol": "tcp", "port_range": "80", "cidr": "0.0.0.0/0", "action": "allow" },
{ "rule_number": 110, "protocol": "tcp", "port_range": "443", "cidr": "0.0.0.0/0", "action": "allow" },
{ "rule_number": 200, "protocol": "tcp", "port_range": "1024-65535", "cidr": "0.0.0.0/0", "action": "allow" }
],
"outbound": [
{ "rule_number": 100, "protocol": "tcp", "port_range": "80", "cidr": "0.0.0.0/0", "action": "allow" },
{ "rule_number": 110, "protocol": "tcp", "port_range": "443", "cidr": "0.0.0.0/0", "action": "allow" },
{ "rule_number": 200, "protocol": "tcp", "port_range": "1024-65535", "cidr": "0.0.0.0/0", "action": "allow" }
]
}
Private Subnet (Internal Only)
{
"inbound": [
{ "rule_number": 100, "protocol": "tcp", "port_range": "1-65535", "cidr": "10.0.0.0/16", "action": "allow" },
{ "rule_number": 200, "protocol": "tcp", "port_range": "1024-65535", "cidr": "0.0.0.0/0", "action": "allow" }
],
"outbound": [
{ "rule_number": 100, "protocol": "tcp", "port_range": "1-65535", "cidr": "10.0.0.0/16", "action": "allow" },
{ "rule_number": 110, "protocol": "tcp", "port_range": "443", "cidr": "0.0.0.0/0", "action": "allow" },
{ "rule_number": 200, "protocol": "tcp", "port_range": "1024-65535", "cidr": "0.0.0.0/0", "action": "allow" }
]
}
Best Practices
- Use rule number gaps — 100, 200, 300 allows insertion without renumbering
- Remember stateless — Add both inbound and outbound rules for bidirectional traffic
- Layer with security groups — NACLs + security groups = defense in depth
- Test before production — Verify rules don’t block legitimate traffic
Next Steps
- Security Groups — Resource-level firewall
- Subnet Management — Subnet creation and routing
- Networking API Reference — Full NACL API