Fatskills
Practice. Master. Repeat.
Study Guide: TECH **AWS Security Groups vs. Network ACLs: Zero-Fluff, Hands-On Guide**
Source: https://www.fatskills.com/aws-certified-solutions-architect-associate/chapter/tech-aws-security-groups-vs-network-acls-zero-fluff-hands-on-guide

TECH **AWS Security Groups vs. Network ACLs: Zero-Fluff, Hands-On Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~9 min read

AWS Security Groups vs. Network ACLs: Zero-Fluff, Hands-On Guide

(For AWS Solutions Architect – Associate & Real-World Cloud Engineers)


1. What This Is & Why It Matters

You’re a cloud engineer at a fintech startup. Your team just migrated a monolithic app to AWS, and now latency spikes are causing payment processing failures. The DevOps lead blames "network security misconfigurations," but no one can pinpoint the issue. Meanwhile, your CISO demands zero public exposure of internal databases.

Here’s the problem: Security Groups (SGs) and Network ACLs (NACLs) are both firewalls, but they work differently—and mixing them up in production can break your app, expose data, or kill performance.


  • Security Groups = Instance-level firewall (like a bouncer at a club—checks IDs at the door).
  • Network ACLs = Subnet-level firewall (like a border checkpoint—inspects all traffic entering/exiting a country).

Why this matters in production:
- If you misconfigure a Security Group, your EC2 instance won’t respond to SSH/RDP, or worse, it’ll accept traffic from the entire internet.
- If you misconfigure a Network ACL, your entire subnet (e.g., all databases in us-east-1a) could lose connectivity to the internet or other VPCs.
- Exam trap: AWS loves asking, "Which firewall is stateful?" (Answer: Security Groups. NACLs are stateless—you must explicitly allow both inbound and outbound rules.)

Real-world scenario:
You inherit a legacy VPC where: - A Security Group allows 0.0.0.0/0 on port 22 (SSH) for "debugging." - A Network ACL denies all inbound traffic to the subnet.
Result? Your EC2 instances are publicly exposed but unreachable—a compliance nightmare.


2. Core Concepts & Components


? Security Group (SG)

  • Definition: Virtual firewall for individual EC2 instances (or other AWS resources like RDS, Lambda, etc.).
  • Stateful: If you allow inbound traffic (e.g., HTTP on port 80), the return traffic is automatically allowed (no need for an outbound rule).
  • Production insight: Always restrict SGs to least privilege (e.g., allow only your ALB’s IP range, not 0.0.0.0/0).
  • Default: Denies all inbound traffic; allows all outbound traffic.

? Network ACL (NACL)

  • Definition: Firewall for entire subnets (applies to all instances in the subnet).
  • Stateless: You must explicitly allow both inbound and outbound traffic (e.g., if you allow inbound HTTP, you must also allow outbound ephemeral ports 1024-65535 for responses).
  • Production insight: NACLs are evaluated before Security Groups—if a NACL denies traffic, the SG never sees it.
  • Default: Allows all inbound/outbound traffic (but custom NACLs deny all by default).

? Key Differences (Cheat Sheet)

Feature Security Group (SG) Network ACL (NACL)
Scope Instance-level Subnet-level
Stateful? ✅ Yes (return traffic auto-allowed) ❌ No (must define inbound + outbound)
Rule Evaluation All rules evaluated (allow/deny) Rules evaluated in order (first match wins)
Default Behavior Deny all inbound, allow all outbound Allow all (default NACL) / Deny all (custom NACL)
Use Case Fine-grained control (e.g., allow only ALB to EC2) Broad subnet protection (e.g., block all SSH to DB subnet)
Performance Impact Minimal (applied at hypervisor) Higher (applied at subnet level)

Production insight:
- Security Groups = "Who can talk to my instance?" - Network ACLs = "What traffic is allowed in/out of my subnet?" - Always use both for defense in depth (e.g., NACL blocks malicious IPs, SG restricts instance access).


3. Step-by-Step Hands-On: Deploy a Secure Web App


Prerequisites

  • AWS account with admin IAM permissions (or at least ec2:*, vpc:*).
  • AWS CLI installed (aws --version).
  • A VPC with public + private subnets (or create one now—see AWS VPC Quickstart).

Goal:

Deploy a public-facing web server (EC2 in a public subnet) and a private database (EC2 in a private subnet), secured with: - Security Groups (allow HTTP to web server, MySQL only from web server).
- Network ACLs (block all SSH to private subnet, allow only ephemeral ports for responses).


Step 1: Create a VPC with Public + Private Subnets

# Create VPC
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text)
aws ec2 create-tags --resources $VPC_ID --tags Key=Name,Value=SecureWebAppVPC

# Create public subnet (us-east-1a)
PUB_SUBNET_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --availability-zone us-east-1a --query 'Subnet.SubnetId' --output text)
aws ec2 create-tags --resources $PUB_SUBNET_ID --tags Key=Name,Value=PublicSubnet

# Create private subnet (us-east-1b)
PRIV_SUBNET_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 --availability-zone us-east-1b --query 'Subnet.SubnetId' --output text)
aws ec2 create-tags --resources $PRIV_SUBNET_ID --tags Key=Name,Value=PrivateSubnet

# Create Internet Gateway + attach to VPC
IGW_ID=$(aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID

# Create route table for public subnet
PUB_RT_ID=$(aws ec2 create-route-table --vpc-id $VPC_ID --query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $PUB_RT_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID
aws ec2 associate-route-table --subnet-id $PUB_SUBNET_ID --route-table-id $PUB_RT_ID


Step 2: Configure Security Groups

Web Server SG (Allow HTTP + SSH)

WEB_SG_ID=$(aws ec2 create-security-group \
  --group-name WebServerSG \
  --description "Allow HTTP and SSH" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# Allow HTTP (port 80) from anywhere
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG_ID \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0

# Allow SSH (port 22) from your IP only (replace X.X.X.X with your public IP)
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG_ID \
  --protocol tcp \
  --port 22 \
  --cidr X.X.X.X/32

Database SG (Allow MySQL only from Web Server)

DB_SG_ID=$(aws ec2 create-security-group \
  --group-name DatabaseSG \
  --description "Allow MySQL from Web Server" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# Allow MySQL (port 3306) only from Web Server SG
aws ec2 authorize-security-group-ingress \
  --group-id $DB_SG_ID \
  --protocol tcp \
  --port 3306 \
  --source-group $WEB_SG_ID


Step 3: Configure Network ACLs

Public Subnet NACL (Allow HTTP, SSH, Ephemeral Ports)

# Get default NACL for public subnet (or create a new one)
PUB_NACL_ID=$(aws ec2 describe-network-acls \
  --filters "Name=association.subnet-id,Values=$PUB_SUBNET_ID" \
  --query 'NetworkAcls[0].NetworkAclId' --output text)

# Allow HTTP (inbound)
aws ec2 create-network-acl-entry \
  --network-acl-id $PUB_NACL_ID \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=80,To=80 \
  --cidr-block 0.0.0.0/0 \
  --rule-action allow \
  --ingress

# Allow SSH (inbound, from your IP)
aws ec2 create-network-acl-entry \
  --network-acl-id $PUB_NACL_ID \
  --rule-number 200 \
  --protocol tcp \
  --port-range From=22,To=22 \
  --cidr-block X.X.X.X/32 \
  --rule-action allow \
  --ingress

# Allow ephemeral ports (outbound, for responses)
aws ec2 create-network-acl-entry \
  --network-acl-id $PUB_NACL_ID \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=1024,To=65535 \
  --cidr-block 0.0.0.0/0 \
  --rule-action allow \
  --egress

Private Subnet NACL (Block SSH, Allow MySQL + Ephemeral Ports)

PRIV_NACL_ID=$(aws ec2 describe-network-acls \
  --filters "Name=association.subnet-id,Values=$PRIV_SUBNET_ID" \
  --query 'NetworkAcls[0].NetworkAclId' --output text)

# Deny SSH (inbound)
aws ec2 create-network-acl-entry \
  --network-acl-id $PRIV_NACL_ID \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=22,To=22 \
  --cidr-block 0.0.0.0/0 \
  --rule-action deny \
  --ingress

# Allow MySQL (inbound, from public subnet)
aws ec2 create-network-acl-entry \
  --network-acl-id $PRIV_NACL_ID \
  --rule-number 200 \
  --protocol tcp \
  --port-range From=3306,To=3306 \
  --cidr-block 10.0.1.0/24 \
  --rule-action allow \
  --ingress

# Allow ephemeral ports (outbound)
aws ec2 create-network-acl-entry \
  --network-acl-id $PRIV_NACL_ID \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=1024,To=65535 \
  --cidr-block 0.0.0.0/0 \
  --rule-action allow \
  --egress


Step 4: Launch EC2 Instances

Web Server (Public Subnet)

WEB_INSTANCE_ID=$(aws ec2 run-instances \
  --image-id ami-0c55b159cbfafe1f0 \  # Amazon Linux 2 (us-east-1)
  --instance-type t2.micro \
  --key-name YourKeyPair \  # Replace with your key pair
  --security-group-ids $WEB_SG_ID \
  --subnet-id $PUB_SUBNET_ID \
  --associate-public-ip-address \
  --query 'Instances[0].InstanceId' --output text)

aws ec2 create-tags --resources $WEB_INSTANCE_ID --tags Key=Name,Value=WebServer

Database Server (Private Subnet)

DB_INSTANCE_ID=$(aws ec2 run-instances \
  --image-id ami-0c55b159cbfafe1f0 \
  --instance-type t2.micro \
  --key-name YourKeyPair \
  --security-group-ids $DB_SG_ID \
  --subnet-id $PRIV_SUBNET_ID \
  --no-associate-public-ip-address \
  --query 'Instances[0].InstanceId' --output text)

aws ec2 create-tags --resources $DB_INSTANCE_ID --tags Key=Name,Value=DatabaseServer


Step 5: Verify Connectivity

  1. SSH into the web server (replace YourKeyPair.pem):
    bash
    ssh -i "YourKeyPair.pem" ec2-user@$(aws ec2 describe-instances --instance-ids $WEB_INSTANCE_ID --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
  2. Test HTTP access (from your browser):
    http://<WEB_SERVER_PUBLIC_IP>
  3. Test MySQL connectivity (from web server to database):
    bash
    mysql -h $(aws ec2 describe-instances --instance-ids $DB_INSTANCE_ID --query 'Reservations[0].Instances[0].PrivateIpAddress' --output text) -u root -p

4. ? Production-Ready Best Practices


Security

  • Least privilege: Never use 0.0.0.0/0 in SGs/NACLs unless absolutely necessary (e.g., public web servers).
  • Ephemeral ports: Always allow outbound ephemeral ports (1024-65535) in NACLs for responses.
  • SSH/RDP: Restrict to your IP or a bastion host (never expose to the internet).
  • Default NACLs: Replace default NACLs (which allow all traffic) with custom ones that deny all by default.

Cost Optimization

  • SGs are free (no cost to use them).
  • NACLs are free (but overusing them can complicate troubleshooting).
  • Avoid "allow all" rules—they increase attack surface and can lead to unexpected costs (e.g., data exfiltration).

Reliability & Maintainability

  • Tag everything: Use Name, Environment, Owner tags for SGs/NACLs.
  • Document rules: Add descriptions to SG/NACL rules (e.g., "Allow ALB to web servers").
  • Use AWS Config to audit SG/NACL changes (e.g., "No SG should allow 0.0.0.0/0 on port 22").

Observability

  • CloudWatch Metrics: Monitor SecurityGroupRulesEvaluated and NetworkAclRulesEvaluated.
  • VPC Flow Logs: Enable for troubleshooting (e.g., "Why is my database unreachable?").
  • AWS Network Firewall: For advanced use cases (e.g., deep packet inspection).


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
SG allows 0.0.0.0/0 on SSH Instance compromised (brute-force attacks). Restrict to your IP or a bastion host. Use AWS Session Manager instead of SSH.
NACL denies ephemeral ports Web server can’t receive responses (e.g., HTTP 504 errors). Explicitly allow outbound ephemeral ports (1024-65535).
SG rule order matters Traffic blocked despite "allow" rule. SGs evaluate all rules (no order). NACLs evaluate rules in numerical order.
Default NACL allows all Subnet exposed to unintended traffic. Replace default NACL with a custom one that denies all by default.
Forgetting stateful vs stateless Database can’t respond to web server. For NACLs, allow both inbound (MySQL) and outbound (ephemeral ports).


6. ? Exam/Certification Focus


Typical Question Patterns

  1. "Which firewall is stateful?"
  2. Security Group (return traffic auto-allowed).
  3. ❌ Network ACL (must explicitly allow inbound + outbound).

  4. "You need to block all SSH traffic to a subnet. Which do you use?"

  5. Network ACL (subnet-level).
  6. ❌ Security Group (instance-level, can’t block entire subnet).

  7. "A web server can’t receive HTTP responses. What’s the issue?"

  8. NACL denies outbound ephemeral ports (1024-65535).
  9. ❌ Security Group (stateful, doesn’t need outbound rules for responses).

  10. "Which is evaluated first: SG or NACL?"

  11. NACL (subnet-level) → SG (instance-level).

Key Trap Distinctions

Concept Security Group (SG) Network ACL (NACL)
Rule Order All rules evaluated (no order) Rules evaluated in numerical order (first match wins)
Default Behavior Deny all inbound, allow all outbound Allow all (default NACL) / Deny all (custom NACL)
Use Case Fine-grained (e.g., allow ALB → EC2) Broad (e.g., block all SSH to DB subnet)


7. ? Hands-On Challenge

Scenario:
You’re troubleshooting a database connection issue. The web server (in a public subnet) can’t connect to the database (in a private subnet). The Security Group allows MySQL (port 3306) from the web server, but the connection times out.

Your task:
1. Identify the issue (hint: check NACLs).
2. Fix it using the AWS CLI.

Solution:


# Check NACL rules for the private subnet
aws ec2 describe-network-acls --filters "Name=association.subnet-id,Values=$PRIV_SUBNET_ID"

# Issue: Outbound ephemeral ports (1024-65535) are not allowed.
# Fix: Add outbound rule for ephemeral ports.
aws ec2 create-network-acl-entry \ --network-acl-id $PRIV_NACL_ID \ --rule-number 100 \ --protocol tcp \ --port-range From=1024,To=65535 \ --cidr-block 10.0.1.0/24 \ # Public subnet CIDR --rule-action allow \ --egress

Why it works:
NACLs are stateless—you must allow both inbound (MySQL) and outbound (ephemeral ports) for responses.



ADVERTISEMENT