Two gatekeepers
A packet reaches an EC2 instance through two layers: the NACL at the subnet boundary, then the Security Group (SG) at the instance.
flowchart LR I((Internet)) --> N["Subnet NACL<br/>100 ALLOW / 200 DENY / * DENY"] N --> S1["SG web-sg"] --> E1[EC2 web] N --> S2["SG db-sg"] --> E2[EC2 db] %% step I: A packet arrives from the Internet. %% step N: The subnet NACL checks numbered rules in order and stops at the first match; it is stateless. %% step S1: The web instance Security Group allows only the ports the web tier needs; it is stateful. %% step E1: Allowed traffic reaches the web instance. %% step S2: The database Security Group allows traffic only from the web tier. %% step E2: The database is never reachable directly from the Internet.
| Security Group | Network ACL | |
|---|---|---|
| Attached to | instance / ENI | subnet (every instance in it) |
| Rule types | Allow only | Allow and Deny |
| State | stateful — return traffic is allowed automatically | stateless — you must open the return direction too |
| Evaluation | checks all rules, then decides | in ascending rule number order, stops at the first match |
| Default | new SG: denies all inbound, allows all outbound | VPC default NACL: allows everything; custom NACL: denies everything |
A VPC's default SG allows inbound from instances in that same SG and outbound to anywhere. An instance launched without an SG gets the default SG.
Ephemeral ports — the most common NACL trap
When a client opens a connection to a server on port 443, the client uses a random source port (an ephemeral port). Linux usually uses 32768–60999, Windows 49152–65535, and NAT Gateway 1024–65535. The server's response goes back to that random port.
- SGs are stateful, so you don't need to worry about the return direction.
- NACLs are stateless: the subnet hosting a web server needs an outbound rule for TCP
1024-65535to clients; the subnet hosting a client that calls out needs an inbound rule for TCP1024-65535. Forget this rule → the connection hangs (timeout).
When to use which
- Opening a port for an application → SG.
- Blocking a specific bad IP → a NACL Deny rule with a lower number than the Allow rule. Space your rule numbers out (100, 110…) so you can insert rules later.
Common commands
| Task | Command |
|---|---|
| Open a port on an SG | aws ec2 authorize-security-group-ingress --group-id sg-xxxx --protocol tcp --port 22 --cidr 203.0.113.10/32 |
| Add a NACL rule | aws ec2 create-network-acl-entry --network-acl-id acl-xxxx --ingress --rule-number 100 --protocol tcp --port-range From=443,To=443 --cidr-block 0.0.0.0/0 --rule-action allow |
| View NACL rules | aws ec2 describe-network-acls --network-acl-ids acl-xxxx |