# Security Groups

## 1. What a Security Group Is

A **security group (SG)** is a **stateful virtual firewall** attached to an elastic network interface (ENI) — so to EC2 instances, ECS tasks (awsvpc), RDS instances, Lambda functions in a VPC, load balancers, VPC endpoints. It decides which traffic may reach (ingress) and leave (egress) that interface.

```
                 ┌──────────── sg-app ────────────┐
alb (sg-alb) ──► │ ingress: tcp 3000 from sg-alb  │──► rds (sg-db: tcp 5432 from sg-app)
                 │ egress:  all (default)          │
                 └─────────────────────────────────┘
```

## 2. The Rules of the Game

- **Allow rules only** — there are no deny rules. Anything not allowed is denied.
- **Stateful**: if inbound traffic is allowed, the **response** is automatically allowed out (and vice versa). You never write rules for ephemeral return ports (unlike NACLs — `02-vpc_subnets_route_tables` §6).
- **All rules are evaluated together** (no order); the most permissive match wins.
- Default SG for a new group: **no inbound**, **all outbound** allowed.
- An ENI can have up to 5 SGs (adjustable); rules from all of them are unioned.
- Changes apply **immediately** to existing and new connections' evaluation (tracked connections keep flowing).

## 3. Sources: CIDR vs Security Group References

```bash
# CIDR source — anyone in that address range
aws ec2 authorize-security-group-ingress --group-id sg-alb --protocol tcp --port 443 --cidr 0.0.0.0/0

# SG reference — any ENI that has sg-alb attached, whatever its IP
aws ec2 authorize-security-group-ingress --group-id sg-app --protocol tcp --port 3000 --source-group sg-alb

# managed prefix list — e.g. CloudFront's origin-facing IP ranges
aws ec2 authorize-security-group-ingress --group-id sg-alb \
  --ip-permissions 'IpProtocol=tcp,FromPort=443,ToPort=443,PrefixListIds=[{PrefixListId=pl-xxxxxxxx}]'
```

**Referencing security groups** is the key design tool: rules describe *roles* ("the app tier may talk to the DB tier") instead of IP addresses, so autoscaling, redeploys and IP changes never require rule updates.

## 4. A Tiered Design

| SG | Ingress | Egress |
|---|---|---|
| `sg-alb` | 443 from `0.0.0.0/0` (or CloudFront prefix list) | 3000 to `sg-app` |
| `sg-app` | 3000 from `sg-alb` | 5432 to `sg-db`, 6379 to `sg-cache`, 443 to the internet (via NAT) for third-party APIs |
| `sg-db` | 5432 from `sg-app` (and `sg-migrations`) | none needed (stateful replies) |
| `sg-cache` | 6379 from `sg-app` | none |
| `sg-bastion` → replace with **SSM Session Manager**: no inbound 22 at all | | |

Tightening **egress** (instead of the default allow-all) limits what a compromised instance can reach — data exfiltration and command-and-control traffic.

## 5. Security Groups vs NACLs

| | Security group | Network ACL |
|---|---|---|
| attached to | ENI (instance-level) | subnet |
| state | **stateful** | **stateless** (return traffic needs explicit rules) |
| rules | allow only | allow **and deny**, numbered, evaluated in order |
| typical use | primary, fine-grained control | coarse subnet guardrails, explicit denies (block an IP range) |

Use SGs for nearly everything; NACLs as an extra layer or for explicit deny lists.

## Under the Hood: Connection Tracking

Statefulness is implemented with **connection tracking** on the host's virtual network: when an allowed flow starts, the 5-tuple (protocol, src IP/port, dst IP/port) is recorded, and packets matching a tracked flow are allowed regardless of rules. Consequences:

- **Removing a rule doesn't cut an established tracked connection** immediately — existing flows continue until they end or time out (unless the rule was an "untracked" all-open rule, a subtle edge case). To kill a compromised session, also use NACLs or stop the instance.
- Tracked connections consume per-ENI **conntrack capacity**; very high connection rates (e.g. a UDP-heavy service) can hit allowance limits, visible as `conntrack_allowance_exceeded` in ENA metrics.
- Some flows are untracked when rules allow all traffic both ways (`0.0.0.0/0` all protocols in and out), which is why tightening rules can surprisingly *increase* conntrack usage.

![An ALB security group admits 443 from the internet, the app security group admits port 3000 only from the ALB group, and the database group admits 5432 only from the app group; rules reference groups rather than IPs and replies are allowed by connection tracking](diagrams/01-sg-tiers.svg)

## Common Pitfalls

1. **`0.0.0.0/0` on SSH/RDP/database ports.**
2. **CIDR rules for internal tiers** instead of SG references — break on IP changes, too broad.
3. **Allow-all egress everywhere** — no exfiltration control.
4. **Thinking SGs can deny** a specific IP — they can't; use NACLs/WAF.
5. **One shared "default" SG** for everything — every resource can talk to every other.
6. **Forgetting that removing a rule doesn't drop established connections.**

## Quick Reference

```
stateful · allow-only · attached to ENIs · rules unioned · default: no in, all out
reference SGs for internal tiers; prefix lists for AWS-managed ranges
ALB(443 public) → app(port from sg-alb) → db(5432 from sg-app)
tighten egress; replace bastions with SSM Session Manager
NACL = stateless subnet guardrail with deny rules
```
