23.6 Security Groups vs NACLs: When to Use Each
Right, let’s settle this. You’ve got these two tools in your AWS toolbox for locking down your VPC: Security Groups and Network ACLs. It’s tempting to think they’re just two ways to do the same thing, but that’s a fast track to a security headache or a 3 AM outage call. One is a bouncer with a guest list; the other is a mindless, automated gate. Knowing which is which is non-negotiable.
The core, can’t-screw-this-up difference is statefulness. Security Groups (SGs) are stateful. If you let a request come in, it automatically allows the response to go back out. Think of it like a thoughtful doorman who remembers your face. NACLs (Network Access Control Lists) are stateless. If an incoming request comes through a gate, the return traffic has to have its own, separate outbound rule to get back out. It’s a turnstile that only works in one direction at a time and has no memory. You have to manage both flows.
The Bouncer: Security Groups (SGs)
SGs are your first, best, and most intuitive line of defense. You attach them to ENIs (think EC2 instances, Lambda functions, etc.), and they operate at the instance level. The rules are ALLOW-only; you can’t write a rule to explicitly deny traffic. You just specify what you want to permit.
The beauty is in the statefulness. Say you want your web server to be able to query a database. You only need to configure the database’s SG to allow inbound traffic on port 3306 from the web server’s SG. You don’t touch the outbound rules. The response traffic is automatically allowed to flow back because the SG remembers the request.
# Create a Security Group for a web server
aws ec2 create-security-group \
--group-name WebServerSG \
--description "Security group for public web servers" \
--vpc-id vpc-1234567890abcdef0
# Add a rule to allow HTTP and HTTPS from anywhere (0.0.0.0/0)
aws ec2 authorize-security-group-ingress \
--group-id sg-903004f8 \ # Your WebServerSG ID
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id sg-903004f8 \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Add a rule to allow SSH access ONLY from your trusted IP
aws ec2 authorize-security-group-ingress \
--group-id sg-903004f8 \
--protocol tcp \
--port 22 \
--cidr 203.0.113.22/32
Best Practice: Never use 0.0.0.0/0 for SSH. I see you. Don’t do it. Reference other security groups instead of IPs wherever possible; it makes your infrastructure more resilient to change.
The Mindless Gate: Network ACLs (NACLs)
NACLs are the dumb, stateless gatekeepers of your entire subnet. They operate at the subnet level, providing a coarse, backup layer of security. This is where you can write explicit DENY rules, which is both powerful and dangerous. Rules are evaluated in order (using a rule number) until a match is found.
Because they’re stateless, you must create mirror-image rules for return traffic. It’s clunky and a common source of “my instance can’t talk to the internet even though the SG is fine” problems.
# Create a NACL for a public subnet
aws ec2 create-network-acl \
--vpc-id vpc-1234567890abcdef0
# Allow ephemeral ports for responses (1024-65535) - OUTBOUND
# This is the rule everyone forgets. It's why things break.
aws ec2 create-network-acl-entry \
--network-acl-id acl-1234567890abcdef0 \
--ingress \
--rule-number 200 \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 0.0.0.0/0 \
--rule-action allow
# Allow HTTP/HTTPS INBOUND
aws ec2 create-network-acl-entry \
--network-acl-id acl-1234567890abcdef0 \
--ingress \
--rule-number 100 \
--protocol tcp \
--port-range From=80,To=80 \
--cidr-block 0.0.0.0/0 \
--rule-action allow
aws ec2 create-network-acl-entry \
--network-acl-id acl-1234567890abcdef0 \
--ingress \
--rule-number 110 \
--protocol tcp \
--port-range From=443,To=443 \
--cidr-block 0.0.0.0/0 \
--rule-action allow
The Pitfall: See that first rule? It’s allowing the return traffic for any connection initiated from an instance in the subnet. If you only create the inbound rules for 80/443 and forget this, the request packets reach your instance but the response packets hit the NACL’s default “* DENY” rule and get dropped. You’re left staring at a timeout.
So, Which One Do I Actually Use?
Use Security Groups for everything you possibly can. They are easier to manage, more flexible (thanks to statefulness and SG referencing), and provide finer-grained control. This is your primary defense.
Use NACLs for the rare cases where you need a subnet-wide, explicit deny—like blocking a specific IP range that you know is malicious. Or, as a mandatory compliance checkbox to enforce a blanket “deny all inbound traffic except 80/443” at the subnet level, knowing your SGs are doing the real work. Think of NACLs as your blunt-instrument, emergency brake, not your steering wheel.
The golden rule: Your Security Groups should define what is allowed. Your NACLs can be used to define what is not allowed, on a broad scale. If you find yourself meticulously crafting NACL rules to permit traffic, you’re probably doing it wrong and making a management nightmare for yourself. Let the bouncer do his job.