SGs vs NACLs
Stateful security groups vs stateless NACLs, rules, ephemeral ports, and the debugging order.
- This builds on The VPC Itself through Routing. Your VPC, subnets, route tables, and IGW must exist.
- In a new terminal session, run the resume script at the end of Routing.
- Read the callout before every step. Predict what will happen, then run it.
Security Groups: Stateful Firewall at the ENI
Goal
Create security groups, understand stateful behaviour, understand source-group referencing, and see exactly what "stateful" means when you block a port.
Estimated time: 1 hour
Create security groups for each tier
What's happening here
A security group is a stateful, virtual firewall attached to an ENI (Elastic Network Interface). Stateful means: if you allow inbound TCP port 443, the outbound response packets are automatically allowed (you don't need a separate outbound rule. Security groups are allow-only) there is no deny rule. If no rule matches, the packet is dropped. Every rule you add is an allow. The way to block something is to simply not have an allow rule for it.
We create three security groups: one for the ALB tier (public), one for the application tier (private), and one for the database tier (isolated). They start with no inbound rules, we'll add them deliberately so each rule's purpose is clear.
# ALB security group, accepts HTTP/HTTPS from the internet
export SG_ALB=$(aws ec2 create-security-group \
--group-name alb-sg \
--description "ALB: accepts 80 and 443 from internet" \
--vpc-id $VPC_ID \
--region $AWS_REGION \
--query 'GroupId' --output text)
aws ec2 create-tags --resources $SG_ALB \
--tags Key=Name,Value=alb-sg --region $AWS_REGION
# App security group, accepts traffic from ALB only
export SG_APP=$(aws ec2 create-security-group \
--group-name app-sg \
--description "App: accepts traffic from ALB SG only" \
--vpc-id $VPC_ID \
--region $AWS_REGION \
--query 'GroupId' --output text)
aws ec2 create-tags --resources $SG_APP \
--tags Key=Name,Value=app-sg --region $AWS_REGION
# DB security group, accepts traffic from App SG only
export SG_DB=$(aws ec2 create-security-group \
--group-name db-sg \
--description "DB: accepts traffic from App SG only" \
--vpc-id $VPC_ID \
--region $AWS_REGION \
--query 'GroupId' --output text)
aws ec2 create-tags --resources $SG_DB \
--tags Key=Name,Value=db-sg --region $AWS_REGION
echo "ALB SG: $SG_ALB"
echo "App SG: $SG_APP"
echo "DB SG: $SG_DB"Inspect the default egress rule every SG gets:
aws ec2 describe-security-groups \
--group-ids $SG_ALB $SG_APP $SG_DB \
--region $AWS_REGION \
--query 'SecurityGroups[*].{Name:GroupName,Ingress:IpPermissions,Egress:IpPermissionsEgress}'Observe: every new security group has one egress rule: allow all outbound (0.0.0.0/0: all protocols). No inbound rules at all. Nothing can reach these resources inbound yet.
Add inbound rules using CIDR ranges
What's happening here
We first add CIDR-based rules to the ALB SG, allowing HTTP and HTTPS from the entire internet (0.0.0.0/0). CIDR rules are simple: allow traffic from this IP range. The --protocol tcp --port 80 and --port 443 rules are independent (each must be added separately. Rule descriptions are optional but important for auditing) six months later you'll want to know why a rule exists.
# ALB: allow HTTP from internet
aws ec2 authorize-security-group-ingress \
--group-id $SG_ALB \
--protocol tcp --port 80 \
--cidr 0.0.0.0/0 \
--region $AWS_REGION
# ALB: allow HTTPS from internet
aws ec2 authorize-security-group-ingress \
--group-id $SG_ALB \
--protocol tcp --port 443 \
--cidr 0.0.0.0/0 \
--region $AWS_REGION
# Verify
aws ec2 describe-security-groups \
--group-ids $SG_ALB \
--region $AWS_REGION \
--query 'SecurityGroups[0].IpPermissions[*].{Proto:IpProtocol,Port:FromPort,CIDR:IpRanges[0].CidrIp}'Checkpoint: ALB SG has two inbound rules. TCP 80 and TCP 443 from 0.0.0.0/0.
Add inbound rules using security group references
What's happening here
Instead of specifying a CIDR range, you can reference another security group as the source. This is more robust than CIDRs because it's identity-based rather than IP-based. When you say "allow traffic from SG_ALB", it means: allow traffic from any ENI that has SG_ALB attached to it. If the ALB scales out and gets new IPs, the rule still works, you don't need to update CIDRs. This is the correct pattern for inter-tier rules in a multi-tier architecture: app accepts from ALB SG, DB accepts from App SG. No CIDR management needed.