NACLS? Ain't nobody got time for that!

Blogs· 6min November 10, 2022

In this blogpost, Adam will try to convince you to implement AWS NACL as additional layer of network protection. He will go through some basics, present some best practices that you could leverage and in the end show how easy it is to implement NACLs in Terraform.


Every engineer that once created EC2 must have stumbled across Security Groups. They're used asstatefulhost firewalls to limit access to EC2 instance, Load balancers and other AWS Compute components. If you ever created EC2 you must have modified at least one Security Group rule. However what about AWS Network Access Control Lists (NACLs)? They're a little bit hidden in the background, forgotten, often omitted on purpose, waiting for you to be used as part of your Network Defense in Depth strategy. Let me walk you through and show how they can be used together with Security Groups to increase the security posture of your VPC and AWS environment.

NACLs - the basics

Let's start with the basics first. AWS NACLs are VPC's security control that act as stateless firewalls that are associated with subnets and control inbound and outbound traffic. They're supposed to supplement Security Groups and should be treated as an additional layer of security, not the only one. As opposed to Security Groups that are stateful, NACLS are stateless, which means you have to do define both incoming and outgoing rules to allow traffic to go through.

Because they are assigned to a subnet they control traffic for all resources associated with that subnet. By default VPCc come with Default NACLs that allow ALL incoming and outgoing traffic. This NACL can be modified and additional rules can be added. Contrary to default NACLs when you create a custom one it will deny both incoming and outgoing traffic until you add proper rules.

Every subnet must have a NACL associated. If you don't associate one the default one will be associated automatically for you. Each subnet can have only one NACL, however every NACL can be associated with many subnets.

Everything (almost?) in AWS comes with a limit, so do the NACLs. The default maximum number of NACLs per VPC is 200 and 20 inbound and 20 outbound rules per NACL (note: ipv4 and ipv6 rules are counted separately). Those are soft limits and can be increased by contacting with AWS Support

Diagram of AWS VPCs, subnets and NACLs

Overview of AWS NACL


Each NACL consists of an ordered list of rules. Rules are evaluated in descending order. When traffic is matched the evaluation stops, regardless of the action taken.

Each ruleset can have the following:

  • Rule Number
  • Type
  • Protocol
  • Port Range
  • Source (inbound) / Destination (outbound)
  • Action
  • Comment

Comment is optional, others are mandatory.

The Rule number must be between 1 and 32766.

Type is the type of the traffic, for example it can be SSH, HTTP, or All IPV4 Traffic.

Protocol is defined as in IANN standard

Port range is the usual TCP/UDP port or port range.

As Source/Destination you can specify CIDR.

Action can be either Allow or Deny.

Best Practices

The one of the biggest advantages of NACL rules is that they can be used to block incoming and outgoing traffic for specific IP address in response to attack or other corporate or regulatory requirements. This cannot be achieved using Security Groups, as you can only allow traffic and not block.

Below you can find an open list of best practices that you can follow to implement NACLs in your environment:

  1. Rules have to be ordered from 1 to 32766. Instead of using sequential numbering you can leave a gap of at least 50 between each rule. This way it will be easier to add more rules later between already existing rules. Remember rules are evaluated in order!
  2. Using Default NACLs should be avoided.
  3. You should be as specific as possible in defining your rules, eg. avoid rules or other broad CIDR ranges.
  4. Avoid rules with All ports for incoming rules.
  5. Remember that NACLs are stateless so define outgoing rules. For that you could use ephemeral port ranges: 5.1. For AWS ELBs 1024-65535 5.2. For Linux servers 32768-61000 5.3. For Windows 49152-65535 5.4. For NAT Gateway and Lambda 1024-65535
  6. Allow SSH (TCP 22) and RDP (TCP/UDP 3389) traffic only from your corporate network
  7. It's good to keep your rules documented for audit purpose and other engineers. Keep the comment brief but explain the reason for each rule
  8. Remember about limits!

NACL and NAT Gateway

If you use NAT Gateways to NAT the traffic from your private networks you might see some strange logs in VPC flow logs. At first glance it might looks like the NAT gateway is accepting traffic from public internet. This could lead to potentially a lot of false positive alerts triggered in your SOC. NAT Gateway will never accept traffic from public internet, however there is one reason that it might look like it does. If you use default NACL or permit all inbound traffic in NACL that is associated with the same subnet as your NAT Gateway the packets will be accepted by NACL, recorded by VPC flow logs but dropped by NAT Gateway. As you cannot associate Security Group with NAT Gateway in order to block such traffic (for example from bots scanning the whole internet all the time) the only way would be to not allow any unnecessary traffic in NACL. If you want to check that in fact this is the case, you can use below query for Cloudwatch Insights:

filter (dstAddr like 'IP_OF_NAT_GW' and srcAddr like 'PUBLIC_IP')
| stats sum(bytes) as bytesTransferred by srcAddr, dstAddr
| limit 10  

if the query returns only traffic from PUBLIC_IP to IP_OF_NAT_GW and not from other way around it means that packets were dropped by NAT gateway.


If you love IaC as we do at FORM3, here is how you can implement NACLs in Terraform:

# Network ACL definition
resource "aws_network_acl" "bar" {
  vpc_id =
  tags = {
    "Name"        = "bar"     
    "Description" = "NACL for public subnets requiring SSH access and outgoing HTTPS traffic"

#NACL subnet association
resource "aws_network_acl_association" "main" {
  network_acl_id =
  subnet_id      =

# This rule allows inbound SSH access from corporate CIDR
resource "aws_network_acl_rule" "bar1" {
  network_acl_id =
  rule_number    = 50
  egress         = false
  protocol       = "tcp"
  rule_action    = "allow"
  cidr_block     = ""
  from_port      = 22
  to_port        = 22

# This rule allows return traffic on the ephemeral port range to corporate CIDR
resource "aws_network_acl_rule" "bar3" {
  network_acl_id =
  rule_number    = 50
  egress         = true
  protocol       = "tcp"
  rule_action    = "allow"
  cidr_block     = ""
  from_port      = 32768
  to_port        = 61000

# This rule allows outgoing traffic on https port to the different subnet
resource "aws_network_acl_rule" "bar4" {
  network_acl_id =
  rule_number    = 100
  egress         = true
  protocol       = "tcp"
  rule_action    = "allow"
  cidr_block     = "" #Subnet CIDR
  from_port      = 443
  to_port        = 443


Using AWS NACLs can increase the general security posture of AWS VPC and the whole environment. It should be treated as an additional security control implemented alongside Security Groups. It's not that hard and can also greatly reduce the amount of false positive alerts and reduce unnecessary traffic.

Written by

Adam Sikora Lead Cloud Security Engineer

Adam is a Cloud Security Lead at Form3. Adam is passionate about automation and implementing cloud native solutions to increase the security posture of the company. In his free time, Adam enjoys taking analog photographs, developing analog films and playing computer games.