Skip to content
AWS aws networking 6 min read

Security Groups vs Network ACLs

When you build a network inside AWS, you get two separate firewalls, and most people confuse them. A security group is a firewall that wraps around an individual resource, like a virtual server. A network ACL (Access Control List, a firewall that sits at the edge of a subnet) wraps around a whole group of resources. They look similar but behave very differently, and using the wrong one in the wrong place is one of the most common reasons traffic mysteriously gets blocked.

The mental model

A Virtual Private Cloud (VPC, your own private network inside AWS) is divided into subnets (smaller slices of that network). Traffic flows from the internet, into a subnet, and then into a specific instance (a running virtual machine).

There are two checkpoints on that journey:

  1. The network ACL guards the door to the subnet. Every packet entering or leaving the subnet is checked here.
  2. The security group guards the door to the instance itself. It is the last check before traffic reaches your server.

A packet must pass both checkpoints. If either one says no, the connection fails.

Security groups

A security group (often shortened to “SG”) is attached directly to a resource such as an Amazon EC2 instance (Elastic Compute Cloud, AWS’s virtual servers) through its network interface. It controls what traffic is allowed in and out of that resource.

Two things make security groups easy to work with:

  • They are stateful. “Stateful” means the firewall remembers connections. If you allow inbound traffic on a port, the reply is automatically allowed back out. You never write a rule for the return trip.
  • They are allow-only. You can only write rules that permit traffic. Anything you do not explicitly allow is blocked by default. There is no concept of a “deny” rule.

When to use this: Security groups are your primary, everyday firewall. Use them to say things like “this web server accepts HTTPS on port 443 from anyone” or “this database accepts traffic on port 5432 only from my application servers.” For almost all access control, this is where you do your work.

Creating a security group rule (Console)

  1. Open the VPC console and choose Security groups in the left menu.
  2. Select your security group (for example sg-0a1b2c3d) and open the Inbound rules tab.
  3. Choose Edit inbound rules, then Add rule.
  4. Set Type to HTTPS, Source to 0.0.0.0/0 (meaning “any IP address”).
  5. Choose Save rules.

Creating a security group rule (CLI)

aws ec2 authorize-security-group-ingress \
  --group-id sg-0a1b2c3d \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0

Output:

{
    "Return": true,
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-0123456789abcdef0",
            "GroupId": "sg-0a1b2c3d",
            "IsEgress": false,
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIpv4": "0.0.0.0/0"
        }
    ]
}

Network ACLs

A network ACL (NACL) is attached to a subnet, not to an instance. Every resource in that subnet shares the same NACL. Each subnet has exactly one NACL, and a default VPC comes with a default NACL that allows all traffic.

NACLs differ from security groups in three big ways:

  • They are stateless. “Stateless” means the firewall does not remember connections. If you allow inbound traffic, the reply is not automatically allowed back out. You must write a separate rule for the return traffic. This is the single most important thing to remember.
  • They support both allow and deny. Unlike security groups, you can write explicit DENY rules. This makes them useful for blocking a known-bad IP address across an entire subnet.
  • Rules are numbered and ordered. AWS evaluates rules from the lowest number to the highest and stops at the first match. A rule numbered 100 is checked before a rule numbered 200.

When to use this: Reach for NACLs as a coarse, subnet-wide backstop, not for day-to-day access control. The classic use is blocking specific malicious IP ranges, or enforcing a broad “no inbound database traffic from the internet” boundary on a private subnet. When NOT to use this: do not use NACLs as your main firewall, and do not try to allow individual applications here. That work belongs in security groups.

The stateless gotcha (read this carefully)

Because NACLs are stateless, a request can arrive successfully but the response can be silently dropped on the way out. When a client connects to your server, the reply goes back to a random high-numbered port on the client called an ephemeral port. Ephemeral ports are temporary ports in the range 1024-65535.

Warning: If your security group allows the traffic but the connection still hangs or times out, a missing NACL outbound rule for ephemeral ports (1024-65535) is almost always the cause. The request gets in, but the reply cannot get out. This is the number-one reason a NACL “silently breaks” a connection that the security group clearly permits.

So to allow inbound HTTPS through a NACL, you actually need two rules:

| Direction | Rule # | Protocol | Port range | Source/Dest | Allow/Deny | Why | | --- | --- | --- | --- | --- | --- | | Inbound | 100 | TCP | 443 | 0.0.0.0/0 | ALLOW | Let the request in | | Outbound | 100 | TCP | 1024-65535 | 0.0.0.0/0 | ALLOW | Let the reply back out |

Adding NACL rules (Console)

  1. In the VPC console, choose Network ACLs.
  2. Select your NACL (for example acl-0a1b2c3d) and open the Inbound rules tab.
  3. Choose Edit inbound rules, Add new rule: number 100, type HTTPS, source 0.0.0.0/0, allow/deny Allow. Save.
  4. Open the Outbound rules tab and Add new rule: number 100, type Custom TCP, port range 1024-65535, destination 0.0.0.0/0, allow/deny Allow. Save.

Adding NACL rules (CLI)

aws ec2 create-network-acl-entry \
  --network-acl-id acl-0a1b2c3d \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=443,To=443 \
  --cidr-block 0.0.0.0/0 \
  --rule-action allow \
  --ingress

aws ec2 create-network-acl-entry \
  --network-acl-id acl-0a1b2c3d \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=1024,To=65535 \
  --cidr-block 0.0.0.0/0 \
  --rule-action allow \
  --egress

Output:

(no output on success; the command returns an empty response)

Side-by-side comparison

FeatureSecurity groupNetwork ACL
Applies toA single instance (network interface)An entire subnet
StateStateful (replies allowed automatically)Stateless (must allow replies yourself)
Rule typesAllow onlyAllow and deny
Rule orderAll rules evaluated togetherNumbered, lowest number wins
Default behaviorDeny all inbound, allow all outboundDefault NACL allows all; custom NACL denies all
Best forPer-application access controlCoarse subnet-wide guardrails

Layering both together

You do not choose one or the other. AWS uses both at the same time, and that is by design. This layered approach is called defense in depth: if one layer is misconfigured, the other still protects you.

A typical setup looks like this: the NACL on a subnet stays wide open (or blocks a few known-bad IP ranges), and all the real, fine-grained rules live in security groups on each instance. This keeps your rules simple and in one place while still giving you a subnet-level safety net.

There is no extra charge for either security groups or NACLs, so layering them costs nothing.

Best Practices

  • Do your everyday access control in security groups; keep NACLs simple.
  • Remember NACLs are stateless. Always add the matching outbound rule for ephemeral ports (1024-65535) when you allow inbound traffic.
  • Reference other security groups as a source instead of IP ranges (for example, allow the database SG to accept traffic only from the app SG). This is cleaner than hard-coding addresses.
  • Number your NACL rules in gaps of 100 (100, 200, 300) so you can insert new rules later without renumbering.
  • Use NACL DENY rules to block specific malicious IPs across a whole subnet, something security groups cannot do.
  • Avoid opening port 22 (SSH) or 3389 (RDP) to 0.0.0.0/0; restrict to your own IP or use a bastion host.
Last updated June 15, 2026
Was this helpful?