Security Groups and Network Access Control Lists (NACLs) are both critical tools for network security in AWS, but they operate at different layers and have distinct characteristics that profoundly affect how you secure your VPC.
Here’s a Security Group in action, attached to an EC2 instance, allowing SSH access from a specific IP address:
{
"SecurityGroups": [
{
"Description": "Allow SSH from my IP",
"GroupName": "my-ssh-sg",
"IpPermissions": [
{
"FromPort": 22,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "203.0.113.45/32"
}
],
"ToPort": 22
}
],
"OwnerId": "123456789012",
"GroupId": "sg-0abcdef1234567890"
}
]
}
This JSON represents a Security Group named my-ssh-sg. It has one inbound rule: it permits TCP traffic on port 22 (SSH) exclusively from the IP address 203.0.113.45. Notice the /32 suffix, which signifies a single IP address.
Now, let’s look at a NACL, applied to a subnet, which is more permissive but still stateful:
{
"NetworkAclId": "acl-0fedcba9876543210",
"Rules": [
{
"CidrBlock": "0.0.0.0/0",
"Egress": [],
"NetworkAclId": "acl-0fedcba9876543210",
"PortRange": {
"From": 1024,
"To": 65535
},
"Protocol": "6",
"RuleNumber": 100,
"RuleAction": "ALLOW",
"IcmpTypeCode": null,
"TrafficType": "ALL"
},
{
"CidrBlock": "0.0.0.0/0",
"Egress": [],
"NetworkAclId": "acl-0fedcba9876543210",
"PortRange": {
"From": 22,
"To": 22
},
"Protocol": "6",
"RuleNumber": 200,
"RuleAction": "ALLOW",
"IcmpTypeCode": null,
"TrafficType": "INBOUND"
},
{
"CidrBlock": "0.0.0.0/0",
"Egress": [],
"NetworkAclId": "acl-0fedcba9876543210",
"PortRange": {
"From": 0,
"To": 65535
},
"Protocol": "-1",
"RuleNumber": 32767,
"RuleAction": "DENY",
"IcmpTypeCode": null,
"TrafficType": "ALL"
}
],
"SubnetId": "subnet-0123456789abcdef0",
"VpcId": "vpc-0abcdef1234567890"
}
This NACL (acl-0fedcba9876543210) has three rules. Rule 100 allows outbound ephemeral ports (1024-65535) for TCP traffic. Rule 200 explicitly allows inbound SSH (port 22) from any IP address (0.0.0.0/0). The crucial part is rule 32767, a DENY rule for all traffic, which acts as the default deny. NACLs are evaluated in order of RuleNumber, and the first match determines the action.
The fundamental problem these two constructs solve is controlling traffic flow into and out of your AWS resources within a Virtual Private Cloud (VPC). Security Groups act as virtual firewalls for your instances, while NACLs act as stateless firewalls for your subnets.
Security Groups are stateful. This means if you allow inbound traffic on a specific port, the return outbound traffic for that connection is automatically allowed, regardless of outbound rules. Conversely, if you allow outbound traffic, the return inbound traffic is also permitted. This simplifies rule management, as you don’t need to explicitly define rules for the return path of an established connection. They operate at the instance level.
NACLs, on the other hand, are stateless. This means inbound and outbound traffic are evaluated separately. You must define explicit rules for both inbound and outbound traffic, and for the return traffic of any established connection. If you allow inbound traffic on port 80, you must also allow outbound traffic on the corresponding ephemeral ports for the response to get back to the source. NACLs operate at the subnet level.
The evaluation order is also a key differentiator. Security Groups evaluate all rules to determine if traffic is allowed. If any rule allows the traffic, it’s permitted. NACLs evaluate rules in numerical order (lowest to highest) and stop at the first match. This means the order of your NACL rules is critical.
Here’s a critical detail that often trips people up: Security Groups are associated with network interfaces, which are typically attached to EC2 instances. A single EC2 instance can have multiple network interfaces, and each can have its own Security Group. NACLs, however, are associated with entire subnets. All instances within a subnet share the same NACL.
A common pattern is to use Security Groups for granular instance-level control and NACLs for broader subnet-level policies, like a default deny for all traffic except for specific whitelisted ports. For example, you might use a NACL to deny all inbound traffic to a subnet by default, then use Security Groups on individual instances to allow only necessary ports like SSH or HTTP.
When troubleshooting network connectivity issues, remember that traffic must pass both Security Group and NACL checks. If an instance cannot be reached, it’s often a matter of checking inbound rules on the Security Group and inbound rules on the NACL associated with the instance’s subnet. Similarly, outbound connectivity issues require checking outbound rules on both.
The one thing most people don’t realize is that the ephemeral port range for return traffic is determined by the operating system of the client or server, and AWS doesn’t dictate this range. It’s typically ports 1024-65535 for TCP and UDP. You must ensure your NACL rules explicitly allow traffic within this range for stateful communication to work correctly when using stateless NACLs.
The next concept to explore is how these security constructs interact with other AWS networking features like VPC Flow Logs and Network Firewalls.