Traffic inside a VPC passes through two layers of defense before reaching an instance — NACL at the Subnet boundary and Security Group at the instance boundary. They look similar on the surface, like “firewall rules,” but they differ in scope, evaluation, and statefulness. Without that distinction, traps such as “outbound is allowed but the response is blocked” show up surprisingly often.
Security Groups
A Security Group (SG) is a set of rules attached to an instance or an ENI (Elastic Network Interface). Multiple SGs can attach to one instance; in that case, rules apply as a union.
The most distinctive trait is that SGs are stateful. Once a connection is allowed, return traffic is permitted automatically — there’s no need to mirror an inbound rule with a matching outbound rule for replies.
SGs are allow-only. There is no deny rule; only what you explicitly allow gets through. The defaults are all-outbound-allowed, all-inbound-denied. A new SG starts with everything blocked from outside, and you add ports and sources one by one as needed.
The source/destination of an SG rule can be a CIDR or another SG’s ID. Inside the same VPC, this lets you express rules like “only resources tagged with this SG can connect,” so rules don’t break when an instance’s IP changes.
NACLs
A NACL (Network ACL) attaches at the Subnet level. One NACL per Subnet, and every resource in that Subnet is governed by the same NACL.
NACLs are stateless. Inbound and outbound rules are completely independent — return traffic for an allowed connection still requires its own explicit rule on the other side. What an SG handles automatically, a NACL requires you to write out.
NACLs support both allow and deny rules. Rules are numbered, evaluated in ascending order, and the first match wins. When you need to block specific traffic explicitly, NACLs are the place to do it.
The defaults split into two cases. The default NACL created with a VPC allows everything, while a custom NACL you create blocks everything to start.
Evaluation Order
Two layers sit between an outside packet and a workload, and they evaluate in the following order:
flowchart LR
Ext["External"] -->|"inbound"| NACL_in["NACL
(Subnet inbound)"]
NACL_in --> SG_in["SG
(Instance inbound)"]
SG_in --> VM["VM"]
VM --> SG_out["SG
(Instance outbound)"]
SG_out --> NACL_out["NACL
(Subnet outbound)"]
NACL_out -->|"outbound"| Ext2["External"]
Inbound traffic from outside passes the NACL at the Subnet boundary first, then is evaluated again at the instance boundary by the SG. Responses go the other way — through the SG outbound, then the NACL outbound. Both layers must permit the traffic for it to make the round trip.
SGs are stateful, so the return path is automatic from the instance side. NACLs are stateless, so the response leg also has to be allowed explicitly. That gap is where the common troubleshooting pitfalls live.
Common Pitfalls
Outbound allowed, response blocked
The NACL allows outbound, but the inbound rule does not allow the destination port of the response. Replies typically arrive on ephemeral ports (1024-65535), and if that range is not covered in NACL inbound, responses get dropped.
This issue is invisible in environments using only SGs because they’re stateful. It surfaces the moment you introduce a NACL into the picture. The fix is to allow ephemeral port ranges in both directions of the NACL.
SG rules look like they aren’t taking effect
If a rule was added but traffic still seems blocked, the more likely cause is the NACL — not another SG conflicting with this one. SGs are per-instance, so allow rules on one SG aren’t undone by another SG. NACLs apply Subnet-wide as a single ruleset.
Default vs custom NACL defaults
The default NACL starts with everything allowed and you remove specific traffic from there. A custom NACL starts with everything denied and you allow specific traffic. Swapping a default NACL for a custom one without re-adding allow rules cuts off all traffic to the Subnet — a common foot-gun.
Comparison
The differences in one table:
| Aspect | Security Group | NACL |
|---|---|---|
| Scope | Instance / ENI | Subnet |
| Statefulness | Stateful (return traffic auto-allowed) | Stateless (return needs its own rule) |
| Rule types | Allow only | Allow + Deny |
| Evaluation | Union of all rules | Numbered order, first match wins |
| Defaults | Inbound denied, outbound allowed | Default NACL: all allowed / custom: all denied |
| Source expression | CIDR or another SG ID | CIDR only |
Vendor Naming Map
| Concept | AWS | GCP | Azure | Alibaba Cloud |
|---|---|---|---|---|
| Per-instance, stateful | Security Group | Firewall Rule (target tag) | NSG (NIC-attached) | Security Group |
| Per-subnet, stateless | Network ACL | (no direct equivalent) | NSG (Subnet-attached) | Network ACL |
GCP doesn’t separate SG and NACL — it has a unified Firewall Rules model with targets such as instance tags, network tags, or service accounts. The mental model is structurally one step different from AWS, Azure, and Alibaba.
Azure’s NSG is the same resource regardless of whether it attaches at the NIC or Subnet level, which is closer to merging AWS’s SG and NACL into one resource.
Series Wrap-up
This closes out the VPC fundamentals series. Four elements combine into the single abstraction called a VPC.
- Isolation (Article 1) — IP space, Subnets, and Tenancy simulating a private network boundary
- Routing (Article 2) — Route Tables deciding traffic paths, IGW and NAT as two kinds of exits
- Connectivity (Article 3) — VPC Peering, Transit Gateway, VPN, and PrivateLink for external connections
- Security (Article 4) — Security Groups and NACLs forming two layers of defense
Vendor names differ, but the abstractions are nearly the same. Build a mental model in one vendor, and moving to another doesn’t disorient.