Security Groups & Network ACLs

AWS · Lesson 14 of 30 7 min read Updated

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-sg group", 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 from alb-sg.
  • db-sg: allows MySQL (3306) only from web-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:

Terminal
aws ec2 create-security-group --group-name db-sg \
  --description "MySQL from web tier only" \
  --vpc-id vpc-0a1b2c3d4e5f67890
Output
{
    "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:

Terminal
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"}]'
Output
{
    "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:

Python
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)}")
Output
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:

Terminal
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/24

Leave gaps between rule numbers (90, 100, 200) so you can insert rules later.

Security groups or NACLs?

Security groupNetwork ACL
Applies toNetwork interfaces (instances, databases, load balancers)Whole subnets
RulesAllow onlyAllow and deny
StateStateful: replies allowed automaticallyStateless: replies need their own rules
EvaluationAll rules togetherIn number order, first match wins
DefaultNo inbound, all outboundDefault 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:

Terminal
aws ec2 describe-security-group-rules \
  --filters Name=is-egress,Values=false \
  --query "SecurityGroupRules[?CidrIpv4=='0.0.0.0/0'].[GroupId, FromPort, ToPort]" \
  --output text
Output
sg-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/0 on 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/0 on 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
Terminal
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?

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?

3. What is the safest way to let web servers reach a MySQL database?

4. Why must custom NACLs allow ephemeral ports outbound?

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