Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 011 / 100

Security groups vs NACLs: a hands-on guide for engineers

Two firewalls, different jobs.

Expona Admin6 min read

When you deploy to AWS, traffic needs rules. You'll encounter two mechanisms that filter network access: security groups and network ACLs. The confusion is common—they sound similar, operate at different layers, and have different mental models. Understanding security groups vs NACLs: a hands-on guide for engineers means knowing when to reach for each tool and why most production systems rely heavily on security groups while treating NACLs as a second line of defense.

Security groups: stateful, per resource

A security group is a stateful firewall attached directly to an EC2 instance, RDS database, load balancer, or Lambda function. "Stateful" means the security group tracks connections. If you allow outbound traffic on port 443, responses on port 443 automatically come back in—you don't need a separate inbound rule for the return traffic.

Let's say you're running a web application on EC2. You want to accept HTTPS traffic from users and send outbound requests to an external API. Your security group might look like this:

bash

# Inbound rules
- Type: HTTPS (port 443), Source: 0.0.0.0/0 (anywhere)
- Type: SSH (port 22), Source: 10.0.1.0/24 (your bastion subnet)

# Outbound rules (default allows all)
- Type: All traffic, Destination: 0.0.0.0/0

Because security groups are stateful, a client on the internet can connect to port 443, and the response flows back automatically. You don't write a rule saying "allow inbound responses on ephemeral ports." The security group remembers the connection.

Security groups are also per-resource. Each EC2 instance, RDS cluster, or ALB has its own security group. This granularity is powerful. Your database security group can restrict access to only port 5432 from your application tier, while your load balancer security group accepts traffic from the internet on port 443.

NACLs: stateless, per subnet

A network ACL (NACL) is a stateless firewall applied to an entire subnet. All traffic entering or leaving a subnet passes through its NACL. "Stateless" means every direction requires an explicit rule. If you allow inbound traffic on port 443, you must also write an outbound rule to allow responses on the ephemeral port range (typically 1024–65535).

Each subnet has exactly one NACL associated with it (though one NACL can be shared across multiple subnets). Rules are evaluated top-to-bottom by rule number, and the first match wins. NACLs also support explicit deny rules, whereas security groups only allow or implicitly deny.

Here's a practical example. Your VPC has a public subnet with a NAT gateway and a private subnet where your application runs. The NACL on the private subnet might enforce:

bash

Inbound rules:
100 - Allow TCP 5432 from 10.0.2.0/24 (application subnet)
110 - Allow TCP 1024-65535 from 0.0.0.0/0 (ephemeral responses)
120 - Deny TCP 3389 from 0.0.0.0/0 (block RDP)
* - Deny all else

Outbound rules:
100 - Allow TCP 443 to 0.0.0.0/0 (HTTPS to internet via NAT)
110 - Allow TCP 1024-65535 to 0.0.0.0/0 (ephemeral responses)
* - Deny all else

In practice, most teams accept the AWS default NACL behavior (allow all) and manage access via security groups instead. The stateless nature of NACLs makes them harder to reason about in day-to-day operations.

Reference security groups instead of IPs

One of the most powerful features of security groups is the ability to reference another security group by ID rather than writing IP addresses. This creates a decoupled, dynamic rule that scales as your infrastructure changes.

Imagine your load balancer sits in front of a fleet of EC2 instances. Both sit in the same VPC. Instead of hardcoding the load balancer's private IP in the EC2 security group's inbound rules, you reference the load balancer's security group:

bash

EC2 instance security group inbound rule:
- Type: TCP, Port: 8080
  Source: sg-0abc1234def56789 (the load balancer security group)

Now, if your load balancer's IP changes, the rule still works. If you scale to multiple load balancers using the same security group, they all have access automatically. This approach is critical in production systems where instances spin up and down constantly.

You can also create circular references. Your load balancer security group can allow inbound traffic from the internet, and your application security group can allow inbound traffic from the load balancer security group. Clean, readable, maintainable.

NACLs can't reference security groups. You're limited to IP ranges. This makes NACLs less dynamic and more brittle in cloud-native architectures.

Most teams only need security groups

In the vast majority of AWS deployments, security groups provide sufficient network isolation. They're stateful (easier to reason about), attached directly to resources (granular control), and support group-based references (scalable).

NACLs serve as a secondary boundary. You'd use them in scenarios like:

  • Explicit deny at the subnet level: You want to block certain traffic even if security groups allow it. Example: deny all traffic from a specific IP range known to be malicious.
  • Compliance or regulatory requirements: Some frameworks mandate multiple layers of network filtering.
  • Protecting against misconfigured security groups: A blanket rule on a NACL can catch overly permissive security group rules.
  • VPC isolation in multi-tenant setups: If you're running isolated workloads in separate subnets, NACLs provide an additional blast radius boundary.

For a typical SaaS platform, e-commerce site, or internal application, security groups alone handle access control. Your architecture might look like:

  • Internet-facing load balancer: security group allows inbound 443 from 0.0.0.0/0.
  • Application tier: security group allows inbound from load balancer security group on port 8080.
  • Database tier: security group allows inbound from application tier security group on port 5432.
  • NACLs: left at default (allow all).

This is clean, auditable, and enough for most teams. You're not juggling stateless rules across multiple dimensions.

Common mistakes

Hardcoding IPs in security group rules: Developers sometimes add rules like "allow inbound from 203.0.113.50" instead of referencing a security group. This breaks when resources migrate, scale, or fail over. Always prefer security group references when both resources are in the same VPC.

Forgetting return traffic in NACLs: If you add an inbound NACL rule for port 443 but forget the ephemeral outbound range, connections will hang. The client sends data in, but the server's response can't leave the subnet. This is a classic gotcha with stateless firewalls.

Overcomplicating with NACLs: Teams sometimes create intricate NACL rules hoping to replace security groups or implement application-layer logic. NACLs operate at the IP level and can't see DNS names, security group tags, or application context. Keep them simple or leave them at default.

Not documenting security group rules: A security group with 20 rules and no comments is a liability. Add descriptions to rules explaining why they exist and what they protect. Six months later, you'll be grateful.

Allowing 0.0.0.0/0 everywhere: It's tempting to open ports to the world for testing. These rules often live longer than intended. Use specific IP ranges or security group references. If you must allow broad access, add a reminder in the description and plan to tighten it.

Key takeaways

  • Security groups are stateful, attached per resource, and support group-based references. Use them as your primary network isolation tool.
  • NACLs are stateless, applied per subnet, and use IP-based rules. Treat them as a second-line defense; most teams leave them at default.
  • Always reference security groups by ID instead of hardcoding IPs. This keeps rules dynamic and resilient to infrastructure changes.
  • When writing NACL rules (rare), remember the stateless model: you need both inbound and outbound rules, including ephemeral port ranges for responses.
  • Document your security group rules with clear descriptions. Future maintainers (including yourself) need to understand intent, not just syntax.
  • Start with security groups. Add NACLs only if compliance, multi-tenancy, or explicit deny requirements demand them.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles