Right, let’s talk about NACLs. If Security Groups are your application’s loyal, detail-obsessed bouncers (checking every single ID at the door), then NACLs are the distracted, easily overwhelmed security guard at the perimeter gate who has a list of rules but keeps forgetting who just walked in or out.

The core, and frankly most annoying, thing to remember about NACLs is that they are stateless. This isn’t a philosophical stance; it’s a technical reality that will bite you if you forget it. Let me explain: a Security Group is stateful. You allow SSH inbound, and the return traffic for that connection is automatically allowed back out, no questions asked. It remembers. NACLs have the memory of a goldfish. If an EC2 instance inside your subnet sends a request out (e.g., to download a software update from the internet), the outbound request might be allowed by the outbound rules. But when the response traffic comes back into the subnet, the NACL has completely forgotten about the original request. That return traffic must be explicitly permitted by an inbound rule. This is the single biggest “gotcha” and the source of most head-scratching “why can’t my instance get to the internet?” problems.

The Anatomy of an NACL Rule

An NACL is just an ordered list of rules, each with a number, and AWS evaluates them in order, lowest number first. The moment traffic matches a rule, it’s either allowed or denied, and the evaluation stops. This is why Rule #100 is the nuclear option: an explicit DENY for everything. You’ll also see a sneaky asterisk (*) at the bottom of every NACL’s rule list in the AWS console. This is the implicit deny, the final boss that catches anything your numbered rules didn’t. You can’t edit or remove it. Your job is to explicitly allow what you want before traffic hits it.

Here’s what a typical, slightly paranoid NACL setup might look for a public subnet. Notice how we have to explicitly allow the return traffic for ephemeral ports.

# Create the NACL for our public subnet
aws ec2 create-network-acl --vpc-id vpc-12345678 --tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=PublicSubnetNACL}]'

# Associate it with the subnet (this is a separate step!)
aws ec2 replace-network-acl-association --association-id acl-assoc-87654321 --network-acl-id acl-abcdef123456

Now for the rules. We’ll set them up using the CLI. The key is getting the order right.

# INBOUND RULES

# Rule 100: Allow HTTP from anywhere (for a web server)
aws ec2 create-network-acl-entry \
  --network-acl-id acl-abcdef123456 \
  --ingress \
  --rule-number 100 \
  --protocol 6 \  # TCP
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --port-range From=80,To=80

# Rule 110: Allow HTTPS from anywhere
aws ec2 create-network-acl-entry \
  --network-acl-id acl-abcdef123456 \
  --ingress \
  --rule-number 110 \
  --protocol 6 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --port-range From=443,To=443

# Rule 120: Allow SSH from my trusted IP only (BEST PRACTICE!)
aws ec2 create-network-acl-entry \
  --network-acl-id acl-abcdef123456 \
  --ingress \
  --rule-number 120 \
  --protocol 6 \
  --rule-action allow \
  --cidr-block 203.0.113.42/32 \  # Your IP here
  --port-range From=22,To=22

# Rule 130: Allow ephemeral ports for return traffic (CRITICAL!)
# This allows return traffic for connections initiated by instances in the subnet.
aws ec2 create-network-acl-entry \
  --network-acl-id acl-abcdef123456 \
  --ingress \
  --rule-number 130 \
  --protocol 6 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --port-range From=1024,To=65535

# Rule 200: The explicit deny-all to catch anything else
aws ec2 create-network-acl-entry \
  --network-acl-id acl-abcdef123456 \
  --ingress \
  --rule-number 200 \
  --protocol -1 \  # All protocols
  --rule-action deny \
  --cidr-block 0.0.0.0/0

# OUTBOUND RULES

# Rule 100: Allow all outbound traffic to anywhere
aws ec2 create-network-acl-entry \
  --network-acl-id acl-abcdef123456 \
  --egress \
  --rule-number 100 \
  --protocol -1 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0

Best Practices and Pitfalls

  1. The Default NACL is a Trap: Every VPC comes with a default NACL that allows everything inbound and outbound. It’s wide open. Do not use it. Create custom NACLs for your subnets. The default is there as a fallback, not a best practice. Using it is like leaving your front door unlocked because the builder said it was okay.

  2. Ephemeral Ports Are Your Problem: As shown above, you must open a wide range of ports (1024-65535) for return traffic. This feels gross and insecure, but it’s the necessary evil of statelessness. The actual security of what can initiate those connections is handled by your Security Groups and the fact that your instance only talks to expected destinations.

  3. NACLs are for Coarse-Grained, Subnet-Level Control: Think of them as a blunt instrument. Use them for things like “block this malicious IP across my entire subnet” or “prevent any traffic from flowing between this dev subnet and that prod subnet.” They are not for application-level logic. That’s what Security Groups are for. If you find yourself writing complex NACL rules to allow traffic between two EC2 instances, you’re doing it wrong. You’ve chosen the wrong tool for the {{< bibleref “Job 4 ” >}}. The Order Matters, Deeply: If you put your allow 0.0.0.0/0 rule at #100 and your deny 192.0.2.0/24 rule at #110, guess what? The traffic from that malicious IP block will be allowed by rule #100 and never even see your deny rule. Order your rules from most specific to least specific. Deny bad IPs first, then allow general web traffic, then have your catch-all deny.

So, when do you actually need an NACL? Honestly, less often than you think. For most applications, meticulously configured Security Groups are more than enough. But for multi-tiered applications where you need an enforced network segmentation (e.g., a truly isolated backend subnet that should never, ever talk to the internet, even if a Security Group is misconfigured) or for compliance mandates that require defense-in-depth at every layer, NACLs are your blunt, forgetful, but ultimately effective tool. Just remember to feed it the right rules in the right order.