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

FeatureNetwork ACLSecurity Group
LevelSubnetResource
StateStatelessStateful
RulesAllow and denyAllow only
OrderEvaluated by rule numberAll rules combined
DefaultDeny allVaries 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 #TypeProtocolPortSource/DestAction
100InboundTCP4430.0.0.0/0Allow
110InboundTCP2210.0.0.0/8Allow
200InboundTCP1024-655350.0.0.0/0Allow
32767Inbound**0.0.0.0/0Deny

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

  1. Use rule number gaps — 100, 200, 300 allows insertion without renumbering
  2. Remember stateless — Add both inbound and outbound rules for bidirectional traffic
  3. Layer with security groups — NACLs + security groups = defense in depth
  4. Test before production — Verify rules don’t block legitimate traffic

Next Steps