---
title: "AWS EC2 Security Groups: The Inbound/Outbound Rules That Will Trip You Up"
description: "Stateful vs stateless, implicit denies, ephemeral ports, IPv6 gaps, the real gotchas in EC2 security group rules that caused me actual production pain."
author: Subhadip Saha
published: 2026-05-27
updated: 2026-05-27
canonical: https://thatdevguy.in/blogs/aws-ec2-inbound-outbound-rules
tags: ["AWS", "EC2", "Security Groups", "Networking", "Cloud"]
---

# AWS EC2 Security Groups: The Inbound/Outbound Rules That Will Trip You Up

Every AWS beginner hits the same wall: you follow the tutorial, open port 80 inbound, and the thing works. Then you go slightly off-script and spend two hours debugging a connection that *should* work but doesn't. This post is everything I wish I'd known before I wasted those hours.

## The Core Misunderstanding: Stateful vs Stateless

Security Groups (SGs) are **stateful**. Network ACLs (NACLs) are **stateless**. This single distinction explains about 80% of the confusion.

**Stateful** means: if you allow traffic in one direction, the return traffic is automatically allowed. You open port 443 inbound? The response packets back to the client go out, no outbound rule needed.

**Stateless** means: every packet is evaluated independently in both directions. No memory of the connection. You open port 443 inbound on a NACL? You *also* need an outbound rule for the ephemeral port range, or the response never leaves.

<Callout type="tip">
Think of Security Groups as a bouncer who remembers faces, if they let you in, they'll let you out. NACLs are a metal detector: every time, both ways, no exceptions.
</Callout>

Most tutorials only use Security Groups, so you never feel the pain. Until you add a NACL and suddenly nothing works.

## Why "Allow All Outbound" Is Not a Security Blanket

The AWS default, and the template every tutorial uses, opens all outbound traffic:

```
Protocol: All
Port range: All
Destination: 0.0.0.0/0
```

This feels like security because you're thinking about it as "requests my server makes." But an attacker who gets code execution on your EC2 instance can:

- Exfiltrate data via HTTP/S to any destination
- Use your instance as a bot in a DDoS attack
- Call out to a C2 (command-and-control) server over any port

Opening all outbound defeats a significant layer of defence-in-depth. The better approach for a web server is:

```
Outbound rule 1: TCP 443 → 0.0.0.0/0   (HTTPS calls to external APIs)
Outbound rule 2: TCP 5432 → sg-xxxxxx   (Postgres in the same VPC, referenced by SG ID)
Outbound rule 3: UDP 53 → 0.0.0.0/0    (DNS)
```

Lock it down to what your app actually needs. Anything else is a hole you're leaving open by default.

## The Implicit Deny, Nothing Is "Allowed by Default"

Security Groups have no concept of "allow unless denied." Every rule is an **allow** rule. Anything without a matching allow rule is **denied silently**, no ICMP "connection refused," no error message, just a timeout.

This is a debugging nightmare because:

1. You open port 80. Works.
2. You add a rule for port 8080. Works.
3. You try port 3000. Nothing. Timeout. You assume your app isn't running.

Your app is fine. Port 3000 just isn't in the security group. The implicit deny is invisible.

<Callout type="warn">
Timeouts when you expect an immediate error are almost always a Security Group or NACL deny. A true "port closed" returns an RST packet instantly. A hanging connection is being silently dropped.
</Callout>

## Source/Destination: CIDR vs Security Group ID

You can specify traffic sources as:
- A CIDR block (`0.0.0.0/0`, `10.0.0.0/16`, your office IP, etc.)
- Another Security Group ID (`sg-0abc123...`)

The Security Group ID option is almost always the right answer inside a VPC. Here's why:

```
# Fragile: CIDR-based (breaks if you add instances or change IPs)
Inbound: TCP 5432 from 10.0.1.0/24

# Robust: SG-based (automatically includes any instance in that SG)
Inbound: TCP 5432 from sg-app-servers
```

When you reference an SG as a source, AWS dynamically resolves to "any ENI currently associated with that security group." You can scale your app fleet from 1 instance to 100 and the database rule never needs updating.

Using CIDRs inside a VPC is technical debt, you'll hit it when you least expect it, usually during an incident at 2am.

## The Ephemeral Port Problem (NACL-Specific)

Back to NACLs. Even if you understand stateless, the ephemeral port range bites you.

When a client connects to your server on port 443, the client picks a random **ephemeral port** (1024–65535) for the response to come back to. Your NACL needs an outbound rule that covers that range:

```
Outbound: TCP 1024-65535 → 0.0.0.0/0
```

Linux kernels typically use `32768–60999`. The IANA standard range is `49152–65535`. AWS documentation recommends allowing `1024–65535` to be safe.

If you forget this, your inbound traffic reaches the instance, your app responds, but the TCP handshake can't complete because the response is blocked by the NACL on the way out.

Here's a quick reference for what you need at the NACL level for a typical public web server:

| Direction | Protocol | Port Range | Purpose |
|-----------|----------|------------|---------|
| Inbound   | TCP      | 80         | HTTP |
| Inbound   | TCP      | 443        | HTTPS |
| Inbound   | TCP      | 1024–65535 | Return traffic for outbound connections |
| Outbound  | TCP      | 80, 443    | HTTP/S for software updates, API calls |
| Outbound  | TCP      | 1024–65535 | Return traffic for inbound connections |
| Outbound  | UDP      | 53         | DNS |
| Inbound   | UDP      | 1024–65535 | DNS response return |

Security Groups don't need any of that, they handle it automatically.

## IPv6: You Need Separate Rules

This one gets people who think they've covered everything.

If your VPC has IPv6 enabled (`::/0`) and your EC2 instance has an IPv6 address, clients on IPv6 networks will try to connect via IPv6. Your IPv4 rule (`0.0.0.0/0`) does **not** cover IPv6 traffic.

You need both:

```
# HTTP
Inbound: TCP 80 from 0.0.0.0/0
Inbound: TCP 80 from ::/0          ← easy to forget

# HTTPS
Inbound: TCP 443 from 0.0.0.0/0
Inbound: TCP 443 from ::/0         ← easy to forget
```

The symptom: works fine from most users, mysteriously fails for users on modern ISPs that default to IPv6 (T-Mobile home internet, many European carriers, anyone on IPv6-only mobile data).

<Callout type="info">
If you're using an ALB in front of EC2, the ALB handles IPv6 termination and forwards as IPv4 to your instances. In that architecture, you only need IPv4 rules on the instance SG, but the ALB SG still needs both.
</Callout>

## The "0 References" Trap

Security Groups can exist with zero instances attached. AWS doesn't warn you when you're editing the wrong one.

Classic scenario:
1. You create `sg-web-prod` and attach it to your instances.
2. Two months later you create a new instance and AWS auto-creates `sg-launch-wizard-1`.
3. You're editing rules in the console. You click on the wrong SG. You add your rule. Nothing changes for your running instances.

Always verify you're editing the right SG by checking the "Associated resources" tab before you start, and check the "Inbound rules" tab on the *instance* Network tab, not just the SG console.

## The Rule That Fixed My Production Outage

True story: had a Lambda → RDS Proxy → Aurora setup where Lambda was timing out. Lambda was in a VPC. The SG on the RDS Proxy was correctly allowing TCP 5432 from the Lambda SG. Everything looked right.

The fix: Lambda's SG needed an **outbound rule** allowing TCP 5432 to the RDS Proxy SG.

SGs are stateful for traffic passing *through* the instance, but for **ENI-to-ENI** traffic inside a VPC, *both ends* need rules. The Lambda ENI needed outbound 5432 → rds-proxy-sg, and the RDS Proxy ENI needed inbound 5432 ← lambda-sg. I only had the second half.

<Callout type="warn">
For VPC-internal communication between managed services (Lambda, RDS, ECS tasks), always check both ends: outbound on the caller and inbound on the callee.
</Callout>

## Checklist Before You File an AWS Support Ticket

- [ ] Confirmed you're editing the SG actually attached to the instance (check instance → Networking tab)
- [ ] If using NACLs: added **both** inbound and outbound rules including ephemeral port ranges
- [ ] If IPv6 is enabled: added `::/0` rules alongside `0.0.0.0/0`
- [ ] For managed services: checked outbound rules on the *caller's* SG, not just inbound on the callee
- [ ] If using SG references: confirmed the instances are actually in the referenced SG
- [ ] Tested with `nc -zv <host> <port>` or `telnet` from within the VPC to isolate SG vs app issue

Most networking issues in AWS are rule omissions, not misconfigurations. When in doubt: check the other end of the connection.
