Skip to main content

Command Palette

Search for a command to run...

AWS Security Groups and NACLs

Updated
4 min read
AWS Security Groups and NACLs
A
DevOps engineer focused on building, automating, and securing cloud infrastructure. I work with AWS, Docker, Terraform, and CI/CD pipelines to ship scalable and reliable systems. I write about cloud services & architecture, deployment strategies, and real-world DevOps practices.

Every request that reaches your application on AWS has passed through at least two security checkpoints you configured - or forgot to configure properly. Security Groups and NACLs are the last line of defense before traffic hits your instances. Get them wrong and everything upstream is irrelevant.


AWS operates on a shared responsibility model. AWS secures the infrastructure - the physical hardware, the hypervisor, the global network. Everything inside your VPC is yours to secure. Security Groups and NACLs are your tools for that.

Understanding not just what they do, but where they operate and how they differ, is what separates a secure architecture from one that just happens to be working right now.


Security Groups: Instance-Level, Stateful

A Security Group is a virtual firewall attached to an individual resource. Traffic is evaluated at the resource level, not the network level.

Stateful: If you allow inbound traffic on port 443, the corresponding outbound response is automatically permitted - you don't write a separate outbound rule for it. AWS tracks the connection state and handles the return traffic for you.

Two things to internalize about Security Groups:

They only support Allow rules. There is no explicit Deny in a Security Group. You allow what you want. Everything else is implicitly denied. This is clean and simple - but it means you can't use a Security Group to block a specific IP while allowing everything else. That's what NACLs are for.

AWS defaults are deliberately restrictive on inbound, permissive on outbound. When AWS creates a default Security Group, it blocks all inbound traffic except SSH (port 22) and allows all outbound traffic except port 25.

The inbound-by-default-deny behavior is correct. The mistake most people make is opening ports they don't need, or using 0.0.0.0/0 as the source when they should be locking it down to a specific CIDR or another Security Group.


NACLs: Subnet-Level, Stateless

Network Access Control Lists operate one level up from Security Groups - at the subnet boundary, not the instance. Every subnet has exactly one NACL. One NACL can be associated with multiple subnets.

Stateless is the critical difference from Security Groups. NACLs have no concept of connection state. If you allow inbound traffic on port 8000, you must also explicitly allow the outbound for the response to leave. Forget the outbound rule and your traffic gets in but the response never reaches the client.

NACLs support both Allow and Deny rules. This is the capability Security Groups don't have - you can explicitly block traffic. Block a known malicious IP range, deny a specific port across the entire subnet, restrict traffic from a specific CIDR. These are subnet-wide rules that apply.

Rules are evaluated in ascending numerical order and evaluation stops at the first match. Rule 100 is checked before rule 200. If rule 100 allows all traffic, rule 200 denying port 8000 will never be evaluated. Rule ordering is everything with NACLs.


How They Work Together

Security Groups and NACLs aren't alternatives - they're complementary layers.

Traffic inbound to an EC2 instance passes through the NACL first (subnet boundary), then the Security Group (instance boundary). Both must allow the traffic for it to reach the application. Either one can block it independently.

Traffic outbound from an instance passes through the Security Group first, then the NACL.

Inbound: Internet -> NACL (subnet) -> Security Group (instance) -> Application Outbound: Application -> Security Group (instance) -> NACL (subnet) -> Internet


Common Mistakes

Opening 0.0.0.0/0 on sensitive ports. SSH (22) and RDP (3389) open to the world is how instances get brute-forced. Restrict SSH to your IP range.

Ignoring NACL statelessness. Allow inbound but forget the outbound ports. Requests arrive, responses never leave.

Rule ordering mistakes in NACLs. Putting an Allow All rule at rule 100 and then trying to deny specific ports at rule 200. The Allow All fires first, the deny never runs.

Treating the default Security Group as safe. The default Security Group allows all traffic between resources that share the same Security Group.


At this point, we understand how Security Groups and NACLs work conceptually. But in real-world systems, the difference becomes obvious only when you observe actual request flows and failure scenarios.

I’ve broken that down with a hands-on walkthrough here: Hands-On: Security Groups and NACLs