Security Groups & Network ACLs
A VPC decides where traffic can go. Security groups and network ACLs decide whether it is allowed. Almost every "my website does not load" or "I cannot connect to my database" question from AWS beginners ends with a missing or wrong rule here, and almost every exposed database on the internet started with a rule that was too open.
In this lesson you will learn how security groups and network ACLs work, how they differ, how to link application tiers safely, and how to audit rules with the CLI. The Cloud Security Basics lesson showed why open ports are dangerous; here you will build the correct rules.
Security groups
A security group is a virtual firewall attached to a network interface: an EC2 instance, an RDS database, a load balancer, a Lambda function in a VPC, and so on.
- Allow rules only. You cannot write a deny rule. Anything not allowed is blocked.
- Stateful. If an inbound request is allowed, the reply goes out automatically, and vice versa. You never write rules for return traffic.
- Inbound and outbound rules. A new security group has no inbound rules (nothing can connect in) and one outbound rule allowing all traffic out.
- Sources can be other security groups. A rule can say "allow port 3306 from any resource in the
web-sggroup", which is safer and easier than listing IP addresses. - Changes apply immediately to every resource that uses the group, and one resource can have several groups.
Chaining tiers with group references
For a classic web app, Isha creates three groups:
alb-sg: allows HTTPS (443) from the internet (0.0.0.0/0) to the load balancer.web-sg: allows port 8080 only fromalb-sg.db-sg: allows MySQL (3306) only fromweb-sg.
Nobody on the internet can reach the web servers or the database directly, and when Auto Scaling adds a new web server with web-sg, it can reach the database at once without any rule change. Create the database group:
aws ec2 create-security-group --group-name db-sg \
--description "MySQL from web tier only" \
--vpc-id vpc-0a1b2c3d4e5f67890{
"GroupId": "sg-0db1234567890abcd",
"SecurityGroupArn": "arn:aws:ec2:ap-south-1:111122223333:security-group/sg-0db1234567890abcd"
}Then allow port 3306 from the web tier's group:
aws ec2 authorize-security-group-ingress --group-id sg-0db1234567890abcd \
--ip-permissions 'IpProtocol=tcp,FromPort=3306,ToPort=3306,UserIdGroupPairs=[{GroupId=sg-0web234567890abcd,Description="web tier"}]'{
"Return": true,
"SecurityGroupRules": [
{
"SecurityGroupRuleId": "sgr-0a1b2c3d4e5f67890",
"GroupId": "sg-0db1234567890abcd",
"GroupOwnerId": "111122223333",
"IsEgress": false,
"IpProtocol": "tcp",
"FromPort": 3306,
"ToPort": 3306,
"ReferencedGroupInfo": {
"GroupId": "sg-0web234567890abcd",
"UserId": "111122223333"
},
"Description": "web tier"
}
]
}For a rule from an IP range, use the shorter form --protocol tcp --port 443 --cidr 0.0.0.0/0. For admin access, prefer Session Manager over opening port 22 at all; if you must use SSH, allow only your own address, for example --cidr 198.51.100.24/32.
Network ACLs
A network ACL (NACL) is a firewall at the subnet boundary. Every subnet has exactly one. They work differently from security groups:
- Allow and deny rules. Useful for blocking a specific bad IP range.
- Numbered rules, evaluated in order. The lowest number is checked first; the first match wins; a final
*rule denies everything else. - Stateless. Return traffic must be allowed explicitly. Replies to clients use temporary ephemeral ports (typically 1024-65535), so outbound rules must allow them.
- The default NACL allows all traffic in and out. A new custom NACL denies everything until you add rules.
This small model shows the first-match logic of NACL rules:
import ipaddress
# (rule number, action, source CIDR, port) - inbound rules of one network ACL
rules = [
(100, "allow", "0.0.0.0/0", 443),
(90, "deny", "198.51.100.0/24", 443),
(200, "allow", "0.0.0.0/0", 80),
]
def evaluate(source_ip, port):
ip = ipaddress.ip_address(source_ip)
for number, action, cidr, rule_port in sorted(rules):
if ip in ipaddress.ip_network(cidr) and port == rule_port:
return f"{action.upper()} (rule {number})"
return "DENY (default * rule)"
for source, port in [("203.0.113.9", 443), ("198.51.100.7", 443), ("203.0.113.9", 22)]:
print(f"{source:<14} port {port:<4} -> {evaluate(source, port)}")203.0.113.9 port 443 -> ALLOW (rule 100) 198.51.100.7 port 443 -> DENY (rule 90) 203.0.113.9 port 22 -> DENY (default * rule)
Rule 90 blocks the bad range even though rule 100 allows everyone, because 90 is checked first. The real command to add that deny rule:
aws ec2 create-network-acl-entry --network-acl-id acl-0a1b2c3d4e5f67890 \
--ingress --rule-number 90 --rule-action deny \
--protocol tcp --port-range From=443,To=443 --cidr-block 198.51.100.0/24Leave gaps between rule numbers (90, 100, 200) so you can insert rules later.
Security groups or NACLs?
| Security group | Network ACL | |
|---|---|---|
| Applies to | Network interfaces (instances, databases, load balancers) | Whole subnets |
| Rules | Allow only | Allow and deny |
| State | Stateful: replies allowed automatically | Stateless: replies need their own rules |
| Evaluation | All rules together | In number order, first match wins |
| Default | No inbound, all outbound | Default NACL allows all |
Use security groups as your main tool. Keep NACLs at their default unless you need subnet-wide deny rules, such as blocking an abusive range or enforcing that a data subnet never talks to the internet.
Audit risky rules
List every inbound rule open to the whole internet in the Region, then check whether any of them is for SSH, RDP or a database port:
aws ec2 describe-security-group-rules \
--filters Name=is-egress,Values=false \
--query "SecurityGroupRules[?CidrIpv4=='0.0.0.0/0'].[GroupId, FromPort, ToPort]" \
--output textsg-0alb234567890abcd 443 443 sg-0old234567890abcd 22 22
Port 443 on the load balancer group is expected. Port 22 open to the world on sg-0old... should be removed with revoke-security-group-ingress. AWS Config and Security Hub can run checks like this continuously.
Common mistakes
- Allowing
0.0.0.0/0on SSH, RDP or database ports "temporarily" and forgetting it. - Using IP addresses between tiers instead of security group references, so new instances break.
- Adding a custom NACL without ephemeral port rules, so requests arrive but replies never leave.
- Trying to write a deny rule in a security group. Use a NACL for denies.
- Debugging the app before checking the rules. If a connection times out, check security groups, NACLs and route tables first.
Key points
- Security groups are stateful, allow-only firewalls on network interfaces; new groups block all inbound traffic.
- Reference security groups in rules to chain tiers: internet to load balancer to app to database.
- Network ACLs are stateless subnet firewalls with numbered allow and deny rules, evaluated lowest first.
- NACLs need explicit rules for return traffic on ephemeral ports.
- Use security groups for everyday control and NACLs for subnet-wide denies.
- Audit for
0.0.0.0/0on sensitive ports regularly, by script or with AWS Config.
Exercise
Write the CLI commands for an alb-sg group that allows HTTPS from anywhere, and a web-sg group that allows port 8080 only from alb-sg. Use vpc-0a1b2c3d4e5f67890 and assume the new group IDs are sg-0alb234567890abcd and sg-0web234567890abcd.
Show solution
aws ec2 create-security-group --group-name alb-sg \
--description "HTTPS from internet" --vpc-id vpc-0a1b2c3d4e5f67890
aws ec2 authorize-security-group-ingress --group-id sg-0alb234567890abcd \
--protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 create-security-group --group-name web-sg \
--description "App port from load balancer only" --vpc-id vpc-0a1b2c3d4e5f67890
aws ec2 authorize-security-group-ingress --group-id sg-0web234567890abcd \
--protocol tcp --port 8080 --source-group sg-0alb234567890abcd--source-group is the short form of a group reference. Because security groups are stateful, no outbound rule is needed for the replies.
Quick Quiz
1. Which statement about security groups is true?
Security groups track connections, so you only write rules for the direction that starts the connection.
2. A NACL has rule 100 allowing port 443 from everyone and rule 90 denying 198.51.100.0/24. What happens to a request from 198.51.100.5 on port 443?
NACL rules are checked from the lowest number, and the first match wins.
3. What is the safest way to let web servers reach a MySQL database?
Group references allow only resources in the web tier, wherever their IP addresses are.
4. Why must custom NACLs allow ephemeral ports outbound?
Without rules for return traffic, responses cannot leave the subnet.
Learn AWS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
