Two firewalls filter traffic inside a VPC. Most people only need to change one of them. Knowing which one, and how each behaves, saves hours of guessing.
The difference in one table
| Security group (SG) | Network ACL (NACL) | |
|---|---|---|
| Attached to | Network interface (instance, RDS, load balancer, Lambda in VPC) | Subnet |
| State | Stateful: replies are allowed automatically | Stateless: you must allow return traffic too |
| Rules | Allow only | Allow and deny, evaluated in rule-number order |
| Default | Inbound: nothing; outbound: all | Default NACL allows all in and out |
| Can reference | Other security groups | CIDR blocks only |
Rule of thumb: do day-to-day filtering with security groups. Leave NACLs at the default allow-all unless you need a subnet-wide deny (for example blocking a known bad range).
A sensible three-tier layout
# load balancer: HTTPS from the internet
aws ec2 authorize-security-group-ingress --group-id sg-alb --protocol tcp --port 443 --cidr 0.0.0.0/0
# app servers: only from the load balancer's SG
aws ec2 authorize-security-group-ingress --group-id sg-app --protocol tcp --port 8080 --source-group sg-alb
# database: only from the app SG
aws ec2 authorize-security-group-ingress --group-id sg-db --protocol tcp --port 5432 --source-group sg-app
Referencing a security group instead of an IP range means new app instances get access automatically and nothing else does.
Warning
Never open SSH (22) or RDP (3389) to
0.0.0.0/0. Use Systems Manager Session Manager or EC2 Instance Connect Endpoint instead and keep those ports closed.
Look at what is there
aws ec2 describe-security-groups --group-ids sg-app \
--query "SecurityGroups[].IpPermissions[].{From:FromPort,To:ToPort,Proto:IpProtocol,CIDR:IpRanges[].CidrIp,SG:UserIdGroupPairs[].GroupId}" --output table
aws ec2 describe-network-acls --filters "Name=association.subnet-id,Values=subnet-0abc" \
--query "NetworkAcls[].Entries[].[RuleNumber,Egress,RuleAction,Protocol,CidrBlock,PortRange.From,PortRange.To]" --output table
NACLs and the ephemeral port trap
Because NACLs are stateless, a rule allowing inbound 443 is not enough. The reply goes back to the client on a random high port, so outbound must allow the ephemeral range (commonly 1024-65535):
aws ec2 create-network-acl-entry --network-acl-id acl-0abc --egress \
--rule-number 120 --protocol tcp --port-range From=1024,To=65535 --cidr-block 0.0.0.0/0 --rule-action allow
Forgetting this is the most common NACL mistake: connections open but replies never arrive.
Debug a blocked connection, step by step
- Is the service listening? On the server:
ss -tlnp | grep 5432(Linux) orGet-NetTCPConnection -LocalPort 5432 -State Listen(Windows). - OS firewall?
firewalld,ufwor Windows Defender Firewall can block before AWS is involved. - Destination SG inbound allows the source IP or SG on that port.
- Source SG outbound allows the destination (default allows all).
- NACLs on both subnets: inbound and outbound, including ephemeral ports.
- Route tables: is there a route between the subnets / to the internet or NAT gateway?
- Let AWS check the path for you:
aws ec2 create-network-insights-path --source i-0app123 --destination i-0db456 --protocol tcp --destination-port 5432
aws ec2 start-network-insights-analysis --network-insights-path-id nip-0abc
aws ec2 describe-network-insights-analyses --network-insights-analysis-ids nia-0abc \
--query "NetworkInsightsAnalyses[].{Reachable:NetworkPathFound,Why:Explanations[].ExplanationCode}"
Reachability Analyzer tells you which component blocks the path.
- Flow logs show
REJECTentries with source, destination and port when traffic is dropped by an SG or NACL.
Quick symptom map
| Symptom | Usually |
|---|---|
| Connection times out | SG or NACL blocks, or no route |
| Connection refused immediately | Reached the host; nothing listening on that port |
| Works from one subnet, not another | NACL on one subnet, or SG references a CIDR not an SG |
| Works on IPv4, not IPv6 | Rules only for 0.0.0.0/0, none for ::/0 |
Prevent it next time
- Name SGs after their role (
sg-app-web,sg-db-orders) and reference SGs rather than IPs. - Enable VPC flow logs on production VPCs.
- Alert on SG changes that open 22/3389 to the world with AWS Config managed rules such as
restricted-ssh.