📅 Day 14 | AWS NACL — Subnet-Level Security in AWS 🔐
Published: (December 12, 2025 at 12:56 PM EST)
3 min read
Source: Dev.to
Source: Dev.to
Overview
- Subnet‑level firewall – attached to a subnet, not to individual EC2 instances.
- Stateless – inbound and outbound rules are evaluated independently; you must allow return traffic explicitly.
- Supports ALLOW and DENY rules – rule order matters (lowest number evaluated first).
- Default NACL – allows all traffic.
- Custom NACL – denies all traffic unless explicitly allowed.
Key Features
| Feature | Description |
|---|---|
| Layer of protection | Secures public and private subnets (e.g., web servers, databases, EKS nodes). |
| Stateless filtering | Requires matching inbound and outbound rules for two‑way communication. |
| Rule types | Both ALLOW and DENY entries are possible. |
| Rule precedence | Rules are processed in ascending order of rule number (e.g., 100 → 101 → 102…). |
| Default behavior | Default NACL permits all traffic; custom NACLs start with a deny‑all stance. |
How NACL Works
- Attachment – A NACL is associated with one or more subnets.
- Stateless nature – If you allow inbound traffic on a port, you must also create a corresponding outbound rule for the response traffic.
- Rule evaluation – The first rule that matches the traffic (by rule number) determines the action.
- Default vs. custom – The default NACL allows everything; a custom NACL must explicitly allow desired traffic and will implicitly deny the rest.
Example Scenarios
Public Subnet
| Resource | Allowed Traffic |
|---|---|
| EC2 web server | HTTP (80), HTTPS (443), SSH (22) |
| Application Load Balancer | Same as above |
Private Subnet
| Resource | Allowed Traffic |
|---|---|
| Application server | Internal traffic to database (3306) |
| Database | Accepts traffic from app subnet on 3306 |
| EKS worker nodes | Node‑to‑node communication within the subnet |
| Note | No direct internet access; all inbound/outbound traffic is filtered. |
Comparison with Security Groups
| Aspect | Security Group | NACL |
|---|---|---|
| Scope | Instance level | Subnet level |
| Statefulness | Stateful (return traffic automatically allowed) | Stateless (return traffic must be allowed explicitly) |
| Rule types | Only ALLOW | ALLOW and DENY |
| Management | Simpler for instance‑specific rules | Useful for high‑level subnet control |
Sample Rule Set
| Rule No | Direction | Protocol | Port Range | Source/Destination | Action |
|---|---|---|---|---|---|
| 100 | Inbound | TCP | 80 | 0.0.0.0/0 | ALLOW |
| 110 | Inbound | TCP | 443 | 0.0.0.0/0 | ALLOW |
| 120 | Inbound | TCP | 22 | 0.0.0.0/0 | ALLOW |
| 1000 | Outbound | ALL | ALL | 0.0.0.0/0 | ALLOW |
| * | * | * | * | * | DENY (implicit) |
Tip: Adjust
vpc_id, CIDR blocks, and rule numbers to match your environment.
Terraform Example
resource "aws_network_acl" "public_nacl" {
vpc_id = aws_vpc.main.id
tags = {
Name = "public-nacl"
}
}
resource "aws_network_acl_rule" "allow_http_in" {
network_acl_id = aws_network_acl.public_nacl.id
rule_number = 100
egress = false
protocol = "6" # TCP
rule_action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
resource "aws_network_acl_rule" "allow_https_in" {
network_acl_id = aws_network_acl.public_nacl.id
rule_number = 110
egress = false
protocol = "6"
rule_action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
resource "aws_network_acl_rule" "allow_ssh_in" {
network_acl_id = aws_network_acl.public_nacl.id
rule_number = 120
egress = false
protocol = "6"
rule_action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 22
to_port = 22
}
resource "aws_network_acl_rule" "allow_all_out" {
network_acl_id = aws_network_acl.public_nacl.id
rule_number = 1000
egress = true
protocol = "-1" # all protocols
rule_action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
References & Further Reading
- GitHub repository:
- Dev.to blog post:
- LinkedIn article:
- Resume (Google Drive): (optional)