Skip to content

Network Access Control Lists#

We've looked at SGs, which secure communications at the resource level. They allow us to control what connections can reach the resource just before it reaches the operating system/kernel (essentially.)

A Network Access Control List (NACL or Network ACL) does it differently by operating at the subnet level. A rule in a NACL can block a connection from even getting into a subnet, before it gets anywhere near a SG or a resource. This means NACLs are your first line of defense.

NACLs are much more primitive than SGs, but they're equally as important. Your main objective when designing a network and protecting it is traffic flow. You have to make sure traffic can only go in the directions it's meant to and everything is blocked. NACLs help achieve this objective.


Let's look at NACLs visually to help us frame our thinking:

An example NACL traffic flow An example NACL traffic flow

An example NACL traffic flow

Here we can see we have two subnets: a public subnet represented by the CIDR range and a second, private subnet represented by The "App Server" in the public subnet is making a connection to TCP/3306 inside of the private subnet on IP The NACL permits this traffic inbound and, assuming the SG on the database also accepts the connection attempt, everything works as expected.

Notice, however, how the connection coming from the private subnet to the public subnet uses *. I'm using * as a wild card to mean "All protocols and ports." One very important "feature" of NACLs is the fact they're not stateful like a SG is. This means when a connection is accepted inbound, all outbound traffic that is related to the original inbound connection is not automatically allowed to flow as you mgiht expect. SGs are stateful, so they do allow the traffic flow to occur for related connections, but NACLs are not stateful and so you need a rule that permits the reverse traffic.

But there's a problem. We know that the MysQL server is listening on port TCP/3306 for inbound connections, but what port is the "App Server" using to receieve the traffic back? There is the potential for it to be any port number inside of the range 1024–65535. You can't possibly know the port the client is using when we write the rules for our NACL, so the only thing you can is allow *, permitting all traffic in that particular direction.

This is why we have SGs - you combine them with NACLs to complete the access control picture:

SGs and NACLs combined SGs and NACLs combined

SGs and NACLs combined


By default, Security Groups (SGs) allow all outbound connections from the resources they're attached to. We've assumed that's the case here and so there are no outbound traffic checks being done by SGs, only inbound.


I've used green connection lines to denote an outbound connection, and blue connection lines to denote an inbound connection.

Step 1.#

An unknown client on the public Internet connects to our App Server on

Step 2.#

Their outbound, green connection is eventually routed to our subnet where the NACL decides if the traffic can come into the subnet. It's allowed via an ALL rule, and the connection becomes a blue, inbound connection for us.

Step 3.#

The SG we have attached to our App Server (EC2 Instance) is allowing TCP/443 form all IP addresses. The connection is allowed through to the EC2 Instance. The connection is also "remembered", because SGS are stateful.

Step 4.#

The App Server needs to talk to the database to serve the client's request. It makes its own outbound connection (green) to the database and encounters the NACL on its subnet. The NACL allows connections to on TCP/3306, so the connection is permitted.

Step 5.#

Now the outbound connection meets the NACL on the database's subnet ( This NACL rule allows connections from on port TCP/3306 so the connection is accepted and becomes an inbound (blue) connection (from the database subnet's perspective.)

Step 6.#

We encounter the SG attached to the database. It permits the traffic to TCP/3306 from The connection is stateful.

Step 7.#

Some data is retreived from the database and it returns that data to the App Server. The SG remembers the connection in Step 6, so the outbound traffic is permitted automatically by the SG attached to the database. The connection then meets the NACL, which is not stateful, and there is a rule that permits all TCP ports to, and the outbound connection from the database is permitted to the App Server subnet.

Step 8.#

The returning data from the database hits an inbound rule on the App Server's subnet NACL. The NACL permits all TCP connections from, so the connection is allowed and becomes an inbound connection for the App Server.

Step 9.#

The App Server got its response from the database and has computed the information the client has asked for. The SG attached to the App Server remembers the original inbound connection and permits the outbound connection automatically.

Step 10.#

The outbound connection then hits the NACL, which permits ALL protocols and ports outbound to all destination (*, essentially.) The connection then becomes an inbound connection for the client.

Step 11.#

The response eventually reaches the client and the connection is closed (or kept open for more data to go back and forth - it depends on the protocol being used.)

Rule numbers#

Unlike SGs, NACLs are not stateful and they behave in a very different way when it comes time to evaluate whether or not a connection is permitted. Because a subnet can only have one NACL attached to it, there is no big list of merged rules like with SGs. Instead, your NACLs rules are given a priority number and are evaluated in that order. Look at a default NACL:

NACL rule numbers

NACL rule numbers

  1. We can see here two rules, one with a rule number of 100 and another with *

Our rule number of 100 is evaluated before the *. If another rule existed with a rule number of 99, it would be evaluated before the rule number 100, and so on. Put your rules in the order that's most important to your design.

Creating a NACL#

Let's go ahead anfd create a single, simple NACL. This is going to be pretty obvious stuff.

To achieve this, you'll need to use the IAM user you created for the Security Group (SG) work in the previous section. Login to the console as that user now and head over to the VPC section (not the EC2 section) of the console:

Creating a NACL

Creating a NACL

  1. This is where you access the NACL console for managing the NACLs (or SGs) inside of your VPCs

Click on "Network ACLs" and you'll get a list of NACLs in your current AWS account for the region you're currently in (set in the console in the top-right):

Creating a NACL

Creating a NACL

  1. We can see a list of our NACLs here
  2. We can see the details of the NACL I have selected in the list, above
  3. And we can see that this NACL is associated with several subnets

In the top-right, we can click "Create network ACL" to begin the process:

Creating a NACL

Creating a NACL

  1. A name is obviously needed for our resources - I'm going with the same format as the SG we created
  2. The VPC ID is required for a NACL to be created - they exist inside of a VPC
  3. And the usual tags we've been attaching to our resources


You do not define the NACL's rules here. That's done after you've created the NACL.

You'll be taken back to the console and your new NACL will be selected. If you look at the inbound and outbound rules, you'll see the defaults that are provided for you. If you select "Edit inbound rules", you'll be presented with a very similar UI to a SG:

Creating a NACL

Creating a NACL

  1. The rule number defines the order in which rules are evaluated. Once a rule is found and matched, evaluation stops
  2. And we can Allow or Deny a connection in NACLs - with SGs we can only Allow as everything is explicitly denied.

A good reason for being able to explicitly deny traffic from particular CIDRs/IPs is you might want to be very explicit in your architecture what is and is not allowed.

You can also also see the default, uneditable rule that Denys all connections regardless of the rules above. It's the final rule and it provides a "Deny by default" like behaviour.