Firewalls and Security Groups
A firewall is a packet filter. It looks at packets and decides allow / deny based on rules over IP, port, protocol, and (for stateful firewalls) connection state.
In cloud, firewalls show up as Security Groups (instance-level, stateful) and NACLs (subnet-level, stateless). On Linux servers it’s iptables / nftables / ufw.
Stateful vs stateless
| Stateful | Stateless | |
|---|---|---|
| Tracks connections? | yes — knows what’s part of an existing flow | no — every packet evaluated independently |
| Need an explicit return rule? | no — return traffic auto-allowed | yes — must allow inbound and outbound |
| Examples | AWS Security Group, modern Linux iptables, home router | AWS NACL, very old packet filters, DDoS scrubbing |
| Memory cost | higher — connection table | lower |
| Easier to reason about? | yes | “yes” if you like writing twice the rules |
Default modern firewalls are stateful. Stateless is what you use when you need maximum throughput or to enforce per-direction policy at a coarser layer.
AWS: Security Groups vs NACLs
Both gate VPC traffic. They run in different places.
| Security Group | NACL | |
|---|---|---|
| Attached to | ENI / instance / RDS / Lambda | subnet |
| Stateful? | yes | no — must allow ingress AND egress |
| Allow + deny? | allow only (default deny) | allow and deny rules, ordered |
| Default | deny all in, allow all out | allow all (until you tighten) |
| Rule reference | other SGs by ID, CIDR | CIDR only |
| Evaluation | all rules across all SGs are unioned | rules processed in order, first match wins |
Security Groups are the primary firewall in a VPC. NACLs are a coarse extra layer (block a malicious IP at the subnet level, defense-in-depth).
Typical SG layout:
sg-web : ingress 80, 443 from 0.0.0.0/0 ; egress all
sg-app : ingress 8000 from sg-web ; egress all
sg-db : ingress 5432 from sg-app ; egress all
Notice rules reference other security groups by ID, not by IP — that means the rule keeps working as instances scale up/down or get new IPs. This is the killer feature vs raw firewall rules.
The “default deny” model
Both Security Groups and most modern firewalls follow allow-listing:
- Start with everything denied.
- Add explicit allows for what you need.
The opposite (block-listing — deny known bad, allow rest) is a bottomless pit because the universe of “bad” is infinite.
iptables / nftables
# Allow established connections (the canonical first rule)
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow SSH from a specific IP
iptables -A INPUT -p tcp --dport 22 -s 198.51.100.5 -j ACCEPT
# Allow HTTP/HTTPS from anywhere
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Drop everything else
iptables -A INPUT -j DROP
nftables is the modern replacement (single tool for IPv4/IPv6, faster). ufw is a friendlier wrapper.
Ingress vs egress
| Direction | What it controls |
|---|---|
| Ingress | inbound — “who can connect TO this host” |
| Egress | outbound — “who/what can this host connect TO” |
Most teams obsess over ingress (web-facing port 443) and forget egress. Egress filtering matters because:
- A compromised app server with open egress can exfiltrate data anywhere.
- Same for SSRF attacks (the app fetches an attacker URL).
- Many compliance regimes (PCI, HIPAA) require egress restrictions.
Sensible egress allowlist for an internal service: outbound to your DB SG, your cache SG, and specific external APIs (by IP if static, or via a proxy). No 0.0.0.0/0.
Security Group composition
You can attach multiple SGs to one instance. The effective rules are the union of all of them. So:
SG-base : ingress 22 from corp-VPN
SG-web : ingress 80, 443 from 0.0.0.0/0
SG-monitor : ingress 9100 from prometheus-SG
instance gets: 22 + 80 + 443 + 9100
Compose like Lego instead of writing one mega-SG per role.
Common interview confusions
- “NACLs replace Security Groups.” — no, they’re complementary. SG is the primary stateful firewall; NACL is a coarse stateless subnet-level extra.
- “My SG egress allows all, why can’t my instance reach the Internet?” — egress rule is necessary but not sufficient. You also need a route to the Internet (Internet Gateway for public, NAT Gateway for private). See 04_nat.md, 09_vpc_cloud_networking.md.
- “My instance has SSH allowed in SG, why is the connection hanging?” — could be NACL, route table, public IP missing, SG allows the wrong CIDR, or your local egress is blocked. SG is one of many layers.
- “Stateful firewall doesn’t need an outbound rule for the response.” — correct. The state table maps the inbound connection and lets responses through automatically.
Diagnosing “blocked by firewall”
The hierarchy of “why can’t A talk to B” in AWS:
- Source SG egress allows it?
- Subnet NACL outbound allows it?
- Route exists from source subnet to destination?
- Subnet NACL inbound (on destination side) allows it?
- Destination SG ingress allows it?
- Application is actually listening?
Any one no = silent drop (firewalls drop, they don’t reply). Use VPC Flow Logs to see which leg fails. See 14_troubleshooting_tools.md.
Interview angle
- “Stateful vs stateless firewall?” — stateful tracks connections so return packets are auto-allowed; stateless evaluates each packet alone, so you must write ingress AND egress rules.
- “Security Group vs NACL in AWS?” — SG is stateful, attached to ENI/instance, allow-only, can reference other SGs. NACL is stateless, attached to subnet, allow+deny ordered, CIDR-only. SG is the primary firewall; NACL is a coarse extra.
- “How would you set up SGs for a 3-tier app (web / app / db)?” — sg-web (ingress 80/443 from 0.0.0.0/0), sg-app (ingress 8000 from sg-web), sg-db (ingress 5432 from sg-app). Reference SGs by ID so it works as instances scale.
- “Why filter egress when ingress is the obvious threat?” — limits blast radius of compromised servers (data exfiltration, SSRF, malware C2), required by many compliance frameworks.
- “Your app can’t reach RDS — walk through the layers.” — app SG egress → app subnet NACL out → route to DB subnet → DB subnet NACL in → DB SG ingress → Postgres listening + accepting that user.