Correct firewall rules are the difference between a private database and a breach, and the first thing to check when traffic fails.
A security group (SG) is a virtual firewall attached to network interfaces (instances, load balancers, RDS, Lambda in a VPC, ECS tasks). Its properties:
- Allow rules only. Anything not allowed is denied.
- Stateful: if inbound traffic is allowed, the response is automatically allowed out, and vice versa.
- New SGs allow all outbound and no inbound traffic.
- A rule's source can be a CIDR, a prefix list or another security group. Referencing SGs is the idiomatic way to build tiers: the database SG allows port 5432 from the app SG, so any instance in the app tier can connect regardless of its IP, and nothing else can.
Changes apply immediately to existing connections' future packets, and one interface can have several SGs (the rules are combined).
Network ACLs sit at the subnet boundary. They are stateless, have numbered rules evaluated in order, and support explicit Deny. Because they're stateless you must allow return traffic yourself, typically the ephemeral port range 1024-65535. The default NACL allows everything, and most teams leave it that way, using SGs for real control and NACLs only for coarse subnet-wide blocks.
When something can't connect, check in this order: route tables, SG inbound on the target, SG outbound on the source, NACLs both ways, then the OS firewall and whether the process listens on the right interface. VPC Reachability Analyzer automates much of this.
Never open 0.0.0.0/0 to SSH, RDP or database ports.
Example
WEB=$(aws ec2 create-security-group --group-name web-alb --vpc-id vpc-0a1b2c3d \
--description "Public HTTPS" --query GroupId --output text)
APP=$(aws ec2 create-security-group --group-name app --vpc-id vpc-0a1b2c3d \
--description "App tier" --query GroupId --output text)
# Internet -> load balancer on 443
aws ec2 authorize-security-group-ingress --group-id "$WEB" \
--protocol tcp --port 443 --cidr 0.0.0.0/0
# Load balancer SG -> app on 8080 (no IPs involved)
aws ec2 authorize-security-group-ingress --group-id "$APP" \
--protocol tcp --port 8080 --source-group "$WEB"
aws ec2 describe-security-group-rules --filters Name=group-id,Values="$APP" \
--query 'SecurityGroupRules[?!IsEgress].[FromPort,ReferencedGroupInfo.GroupId]' --output text
# 8080 sg-0web...Remember
- Security groups: allow-only, stateful, attached to interfaces
- Reference security groups as sources to build tiers
- NACLs: stateless, ordered, can deny, apply to whole subnets
- Debug connectivity: routes, SGs, NACLs, then the host
Watch out for
- Adding a NACL rule for inbound traffic but forgetting the outbound ephemeral ports for responses.