Security Groups vs NACLs on AWS

February 13, 2026 | AWS Security Networking

Stateful vs stateless, defense in depth.

Security Groups vs NACLs: Defense in Depth

AWS provides two layers of network firewall: Security Groups (instance-level, stateful) and Network ACLs (subnet-level, stateless). Understanding when to use each — and how they complement each other — is essential for a robust security posture.

Security Groups: Stateful Firewalls

Security Groups operate at the ENI (Elastic Network Interface) level:

  • Stateful — If you allow inbound traffic, the response is automatically allowed outbound
  • Allow rules only — You cannot create deny rules; anything not explicitly allowed is denied
  • Evaluate all rules — All rules are evaluated before deciding (no ordering)
  • Instance-level — Applied directly to EC2 instances, RDS, Lambda, etc.
# Web server Security Group
aws ec2 create-security-group --group-name web-sg --description "Web servers"
aws ec2 authorize-security-group-ingress --group-id sg-xxx \
  --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-xxx \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

# Database Security Group - only allow from web-sg
aws ec2 authorize-security-group-ingress --group-id sg-db \
  --protocol tcp --port 5432 --source-group sg-xxx

NACLs: Stateless Firewalls

Network ACLs operate at the subnet level:

  • Stateless — Inbound and outbound rules are evaluated independently; you must allow both directions
  • Allow and deny rules — You can explicitly deny specific traffic
  • Ordered evaluation — Rules are processed in order by rule number (lowest first)
  • Subnet-level — Applied to all resources in the subnet
# NACL for data subnet - allow PostgreSQL from private subnets only
aws ec2 create-network-acl-entry --network-acl-id acl-xxx \
  --rule-number 100 --protocol tcp --port-range From=5432,To=5432 \
  --cidr-block 10.0.10.0/24 --rule-action allow --ingress

# Deny all other inbound
aws ec2 create-network-acl-entry --network-acl-id acl-xxx \
  --rule-number 200 --protocol -1 \
  --cidr-block 0.0.0.0/0 --rule-action deny --ingress

# Allow return traffic (stateless - must be explicit)
aws ec2 create-network-acl-entry --network-acl-id acl-xxx \
  --rule-number 100 --protocol tcp --port-range From=1024,To=65535 \
  --cidr-block 10.0.10.0/24 --rule-action allow --egress

Side-by-Side Comparison

FeatureSecurity GroupsNACLs
LevelInstance (ENI)Subnet
StatefulnessStatefulStateless
RulesAllow onlyAllow and Deny
EvaluationAll rulesOrdered (by number)
DefaultDeny all inboundAllow all
ScopePer-resourcePer-subnet

Defense in Depth Strategy

  1. Security Groups (primary control) — Fine-grained, per-resource rules. This is where 90% of your firewall logic lives.
  2. NACLs (secondary control) — Broad subnet-level rules for additional protection:
    • Block known malicious IP ranges
    • Restrict data subnets to private subnet CIDR only
    • Add explicit deny rules that Security Groups can't provide

Best Practices

  • Reference Security Groups by ID — Use source Security Group references instead of IP ranges for inter-tier communication
  • Keep NACLs simple — Only add rules that Security Groups can't handle (explicit denies)
  • Name everything — Use descriptive names and tags for both SGs and NACLs
  • Audit regularly — Use AWS Config rules to detect overly permissive Security Groups
  • Avoid 0.0.0.0/0 on database ports — This is the #1 security misconfiguration in AWS

Eazy SaaS Tip: We use Security Groups as the primary firewall with NACLs providing a safety net. Our standard VPC template includes locked-down NACLs on data subnets that only allow traffic from application subnets — even if someone misconfigures a Security Group, the NACL provides protection.