AWS · commonly confused
Security group vs Network ACL
Short answer
First published
On this page · 10 sections
In simple terms
Both control traffic in a VPC, and they sit at different places. A security group is attached to the instance; a network ACL is attached to the subnet, so it applies to everything in it, including instances launched later by someone who forgot the security group.
The difference that actually causes outages is memory. A security group remembers that a connection was allowed in, so the reply goes out without being asked about. A network ACL remembers nothing: it judges every packet on its own, and a reply is just another packet it has never seen before.
That is why a network ACL needs two rules for one conversation. Allow 443 inbound so the request arrives, and allow the ephemeral port range outbound so the answer can leave.
Words used on this page
- security group
- A set of allow rules attached to an instance or another resource in the VPC. It is stateful and has no deny rules.
- network ACL
- A numbered list of allow and deny rules attached to a subnet. It applies to everything in that subnet and is stateless.
- stateful
- Remembers connections it has allowed, so replies to them are let through without a rule of their own.
- stateless
- Judges every packet on its own. A reply is just another packet, so it needs a rule that allows it.
- ephemeral port
- The temporary port the client picks for its end of a connection. Replies from a server go to it, so a network ACL needs an outbound rule for the client's range.
- rule number
- A network ACL rule's position, from 1 to 32766. Rules are checked from the lowest number and the first match decides.
- asterisk rule
- The last rule in every network ACL, shown as *, that denies anything no numbered rule matched. It cannot be deleted.
- connection tracking
- How a security group remembers allowed connections. Changing a rule does not cut a connection it is already tracking.
- VPC Flow Logs
- Records of the traffic to and from network interfaces, with ACCEPT or REJECT for each flow. The quickest way to see where a packet was dropped.
What actually happens
AWS states the difference as five properties. A security group operates at the instance level, takes allow rules only, evaluates all rules before deciding, and return traffic is "automatically allowed (stateful)". A network ACL operates at the subnet level, takes allow and deny rules, "evaluates rules in ascending order until a match for the traffic is found", and return traffic "must be explicitly allowed (stateless)".
Two consequences follow from the evaluation difference. Because a security group evaluates every rule and has no deny, rule order is meaningless and you cannot carve an exception out of a broader allow. Because a network ACL stops at the first match, order is everything — a deny at rule 100 is final even if rule 200 would have allowed the same packet.
A network ACL rule is numbered 1 to 32766, and every ACL carries an unnumbered * rule at the end that denies anything unmatched. A custom network ACL starts with only that rule, so until you add others it denies traffic in both directions.
The ephemeral range depends on whichever client opened the connection, not on your server. AWS documents many Linux kernels, including Amazon Linux's, using 32768-61000, Windows Server 2008 and later 49152-65535, and Elastic Load Balancing, NAT gateways and Lambda all using 1024-65535. Its own example rules use 32768-65535, and it suggests opening 1024-65535 to cover the range of clients that might reach a public-facing instance.
A network ACL also does not filter everything. AWS lists DNS through the Route 53 Resolver, DHCP, EC2 instance metadata, ECS task metadata, Windows license activation and the Amazon Time Sync Service as traffic it cannot block — so it is not a complete egress control.
One HTTPS conversation, expressed in both
# Security group — one rule. The reply needs no mention.
Inbound: TCP 443 from 0.0.0.0/0 ALLOW
# Outbound is left at its default allow-all. Even if it were empty,
# the response to an allowed inbound connection would still go out,
# because the group is stateful.
# Network ACL — the same conversation needs two rules.
Inbound:
110 TCP 443 0.0.0.0/0 ALLOW # the request arrives
* All traffic 0.0.0.0/0 DENY # unnumbered, not removable
Outbound:
140 TCP 32768-65535 0.0.0.0/0 ALLOW # the reply leaves
* All traffic 0.0.0.0/0 DENY
# Delete outbound rule 140 and inbound 443 still passes. The request
# arrives, the server answers, and the answer is dropped on the way out.32768-65535 is the range in the AWS example. Which range you need depends on the client: a Windows Server 2008+ client uses 49152-65535 and a request through a NAT gateway or a load balancer uses 1024-65535.
Try it yourself
Before you start
- An AWS account and VPC where you are allowed to make changes, ideally a test VPC. A network ACL change applies to every instance in the associated subnets, and a wrong one cuts them all off, including your own SSH session.
- IAM permissions to read and edit network ACLs and security groups (EC2 API actions such as
DescribeNetworkAcls,CreateNetworkAclEntryandDescribeSecurityGroups) and to read VPC Flow Logs. - VPC Flow Logs turned on for the subnet or the instance's network interface before you test. They only show traffic from after they were created, aggregated over up to 10 minutes by default.
- A list of what talks to the subnet: internet clients, load balancers, NAT gateways, Lambda functions. Their ephemeral port ranges differ, and the outbound rules have to cover all of them.
- A copy of the current rules (from the console, or
aws ec2 describe-network-acls) so you can put them back if a change locks you out.
Seeing a stateless drop in VPC Flow Logs
Documented, not reproduced: nothing was run against an AWS account. The scenario and both records are quoted verbatim from the Amazon VPC User Guide's flow log record examples. A home computer at 203.0.113.12 pings an instance whose network interface has the private address 172.31.16.139. The security group allows inbound ICMP but not outbound; the network ACL also allows inbound ICMP but not outbound. The output is not what ping prints: it is the two records in the instance's flow log, in the default format. The account ID, interface ID and addresses are AWS's documentation examples, not a real account.
Command
ping 172.31.16.139
Output · as shown in Amazon VPC: Flow log record examples
2 123456789010 eni-1235b8ca123456789 203.0.113.12 172.31.16.139 0 0 1 4 336 1432917027 1432917142 ACCEPT OK 2 123456789010 eni-1235b8ca123456789 172.31.16.139 203.0.113.12 0 0 1 4 336 1432917094 1432917142 REJECT OK
How to read it
- The default fields, in order: version, account ID, interface ID, source address, destination address, source port, destination port, protocol, packets, bytes, start and end (Unix seconds), action, log status. Protocol 1 is ICMP, which has no ports, so both are 0.
- The first record runs from 203.0.113.12 to 172.31.16.139 and says ACCEPT: the ping got past both the network ACL and the security group and reached the instance.
- The second runs the other way, from the instance back to 203.0.113.12, and says REJECT. The security group let the reply out because it remembered the ping; the network ACL judged the reply on its own, found no outbound rule for it, and dropped it.
- ACCEPT one way and REJECT the other for the same pair of addresses is the signature of a missing return rule. AWS notes that if the security group had blocked the ping, there would be a single REJECT record instead.
- For HTTPS, the reply that gets the REJECT is the server's answer to the TCP handshake, sent from port 443 to the client's ephemeral port. That is why a missing outbound ephemeral rule stops the connection before the application ever sees a request.
The mistake this causes
Allowing a port inbound on a network ACL and leaving the ephemeral range closed outbound.
Why people do it The security group is stateful, so this is a habit that works everywhere else in the VPC. A network ACL keeps no state, so the reply is judged as its own packet, matches no outbound rule, and hits the final deny.
What you see The connection hangs and then times out rather than being refused. For TCP, the first packet dropped is the server's reply to the opening handshake, so the connection never opens and the application logs nothing; the server is left with half-open connections in the SYN-RECEIVED state.
If it goes wrong
- New connections hang and time out after a custom network ACL is associated, though the security group has not changed
- Add the outbound rule for return traffic: TCP to the clients' ephemeral ports, 1024-65535 if load balancers, NAT gateways or Lambda are among them. A new custom ACL blocks everything in both directions until you add rules, so check inbound too.
- IPv4 clients connect and IPv6 clients time out
- IPv4 and IPv6 rules are evaluated separately, and none of the IPv4 rules apply to IPv6. Add the matching
::/0rules in both directions, as AWS's example ACL does with rules 105, 115 and 145. - Load balancer health checks fail for instances in the subnet
- AWS warns that a deny rule for all traffic from 0.0.0.0/0 or from the subnet's own CIDR stops the load balancer's health checks. Allow the load balancer's subnets in, and allow replies to them on 1024-65535, the range Elastic Load Balancing uses.
- A new deny rule in the network ACL has no effect
- Rules are checked from the lowest number and the first match wins. Give the deny a lower number than the allow that covers the same traffic.
- The flow log shows nothing for the test you just ran
- Flow logs show nothing until there is traffic after they were created, and records are aggregated over up to 10 minutes by default (1 minute if you chose it when creating the flow log). Send the traffic again and wait for the interval.
How it shows up in production
A team adds a custom network ACL to a public subnet to satisfy a control that asks for subnet-level filtering. They copy the security group's inbound rules across — 80 and 443 — and leave outbound alone.
Nothing breaks at first, because a new custom network ACL applies to no subnet until it is associated, and the subnet keeps the default ACL, which allows everything. The next deploy makes the association, and the site stops responding.
The investigation goes to the application first and finds nothing: its logs show no new requests at all, because each connection fails at the TCP handshake before the application sees it. The server's handshake replies are leaving the instance and being dropped at the subnet boundary, which is the one place neither the application logs nor the security group configuration will mention.
VPC Flow Logs show it: an ACCEPT on the inbound flow and a REJECT on the return flow. That asymmetry is the signature — a security group that blocks the request shows a single REJECT on the inbound flow instead.
How to tell which one you are hitting
- Does the connection fail immediately, or hang and time out?
- An immediate refusal means something answered and said no: nothing listening on the port, or a host firewall rejecting it. A hang that ends in a timeout means packets are being dropped silently, which is what a security group, a network ACL's deny rule and a routing problem all do, so check all three.
- Do the server's logs show the request arriving?
- For TCP, a blocked return path stops the handshake, so the application logs nothing, just as with a block on the way in. Look one level down: connections stuck half-open (
ss -tn state syn-recvon Linux), or flow log records with an inbound ACCEPT and an outbound REJECT, mean the replies are being dropped. A security group cannot cause that, because it allows the reply to any connection it let in. - Is it only some clients?
- That can mean the outbound ephemeral range is too narrow rather than missing. A rule for 32768-61000, the common Linux range, drops Windows Server 2008+ clients above 61000 (they use 49152-65535). A range starting at 32768 drops anything using lower ports, such as NAT gateways and Elastic Load Balancing, which use 1024-65535.
- Do you need to block a specific address?
- Then, of these two, it has to be a network ACL. Security groups have no deny rule, so there is no way to allow a range and exclude part of it.
Security considerations
- Removing a security group rule does not cut connections it already allowed: AWS says tracked connections are not immediately interrupted and carry on until they time out. To stop an attacker's open connection now, add a network ACL deny, which breaks existing connections in either direction.
- Opening 1024-65535 outbound, as AWS suggests for public-facing instances, also lets instances in the subnet start new connections to those ports, because a stateless ACL cannot tell a reply from a new connection. Keep the security group's outbound rules as the precise control, and put ACL denies for ports you never want used before the wide allow.
- A network ACL is not a complete egress control. It cannot block DNS through the Route 53 Resolver, instance metadata, DHCP, ECS task metadata, Windows license activation, the Time Sync Service or the VPC router's reserved addresses. Use Route 53 Resolver DNS Firewall for DNS and the instance metadata options for metadata.
- Because the ACL covers the whole subnet, it still protects an instance launched with the wrong security group, which is why AWS suggests it as defence in depth. The flip side is that one bad edit cuts off every instance in every associated subnet at once, so change ACLs through reviewed infrastructure code, not by hand in the console.
In an interview
How this gets asked in an interviewpreparation
Usually phrased as What is the difference between a security group and a network ACL?
What a strong answer contains Stateful versus stateless is the answer that matters, and the test of whether someone has used both is whether they mention ephemeral ports unprompted. A strong answer adds the two evaluation differences — all rules versus first match, and allow-only versus allow and deny — and says where each belongs: the security group as the primary control, the ACL as a subnet-level guard rail or for the denies a security group cannot express.
The follow-up "Your instance serves traffic fine, then you attach a custom network ACL allowing 443 inbound and it stops. Why?" The reply has no outbound rule, so it is dropped leaving the subnet. Worth saying that a custom ACL denies everything until told otherwise, unlike the default one.
Sources
The behaviour this page describes, as documented by the projects themselves.
- Amazon VPC: Create a network ACL
- Amazon VPC: Flow log limitations
- Amazon VPC: Security group rules
- Amazon EC2: Security group connection tracking
- RFC 9293: Transmission Control Protocol (TCP)
- Amazon VPC — Compare security groups and network ACLs
- Amazon VPC — Control subnet traffic with network access control lists
- Amazon VPC — Custom network ACLs for your VPC
- Amazon VPC: Flow log record examples
Terms used here (1)plain definitions
Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.
- Firewall
- A network security system that monitors and controls incoming and outgoing traffic based on predetermined rules. In cloud environments, firewalls are often implemented as security groups or network ACLs that restrict access to specific ports and IP ranges.
Did this get you to an answer?
No text box on purpose — please do not paste production logs anywhere
Something wrong or out of date? Report it