Let's Connect

Network cabling and switch ports in a data center rack, representing EC2 outbound network paths

An app on EC2 that cannot reach an external API, an SMTP server on port 587, or a package mirror is almost always hitting EC2 blocked outbound TCP at one of four layers, not a dead remote service. The tell is the failure mode: the connection hangs and times out instead of getting refused. A refused connection means you reached the host and it said no. A hang means a packet was silently dropped somewhere on the path. The fix is to walk the four layers in order — security group egress, network ACLs, the route table, then the host firewall — and confirm which one is eating your SYN packets.

How do I confirm the connection is actually blocked and not just slow?

Diagnose before you change anything. You want to distinguish a hang (something dropped the packet) from connection refused (you reached the service). Run a raw TCP probe to the exact host and port your app is failing on. If you have netcat, use it; if not, bash on a stock Amazon Linux or Ubuntu AMI can open a TCP socket through /dev/tcp.

bash
# netcat: -z scan only, -v verbose, -w 5 timeout in seconds
nc -zv -w 5 api.stripe.com 443

# curl with timing, no dependency on nc being installed
curl -v --connect-timeout 5 https://api.stripe.com

# pure-bash TCP test when nc/curl are missing (works for SMTP 587, etc.)
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/smtp.sendgrid.net/587'; echo "exit=$?"
# exit=0   -> TCP handshake succeeded, path is open
# exit=124 -> timeout hit: packet dropped (firewall/route), keep reading
# exit=1   -> connection refused: you REACHED the host, not a network block

An exit code of 124 from timeout, or curl sitting on 'Trying x.x.x.x...' until it dies, is your signal that a firewall or routing layer is dropping traffic. Now you know it is worth walking the layers. If you instead got 'connection refused', the network path is fine and the problem is the remote service or the wrong port — stop here and go look at that.

Layer 1: Is the security group blocking egress?

Security groups are stateful. If an outbound connection is allowed, the return traffic is automatically permitted — you never write an inbound rule for replies. The default SG attached at launch allows all egress (0.0.0.0/0, all ports), so on a fresh instance this layer is rarely the culprit. The bite comes from a hardened SG where someone replaced the wide-open egress rule with a tight allowlist and forgot the port the app actually needs. Check what egress rules exist, then add the missing one.

bash
# inspect the egress rules on your SG
aws ec2 describe-security-groups \
  --group-ids sg-0abc123def456 \
  --query 'SecurityGroups[].IpPermissionsEgress'

# allow outbound HTTPS (443) to anywhere
aws ec2 authorize-security-group-egress \
  --group-id sg-0abc123def456 \
  --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges='[{CidrIp=0.0.0.0/0,Description="https out"}]'

# allow outbound SMTP submission (587)
aws ec2 authorize-security-group-egress \
  --group-id sg-0abc123def456 \
  --ip-permissions IpProtocol=tcp,FromPort=587,ToPort=587,IpRanges='[{CidrIp=0.0.0.0/0,Description="smtp submission"}]'

Layer 2: Are the Network ACLs dropping the return traffic?

This is the layer that fools experienced people, because NACLs are stateless. Unlike a security group, a NACL evaluates outbound and inbound traffic independently — allowing the outbound connection does nothing for the reply. The remote service answers from its port back to a high-numbered ephemeral port on your instance (Linux uses 32768-60999, but allow the full 1024-65535 to be safe). If your inbound NACL does not allow that ephemeral return range, your SYN leaves, the SYN-ACK comes back, and the NACL drops it. The result is a clean hang that looks exactly like Layer 1 or 3.

bash
# dump the NACL rules for the subnet your instance lives in
aws ec2 describe-network-acls \
  --filters Name=association.subnet-id,Values=subnet-0aa11bb22cc33 \
  --query 'NetworkAcls[].Entries'

# you need BOTH directions. typical working pair:
#   Egress=true,  RuleNumber=100, Protocol=6(tcp), PortRange=443,         RuleAction=allow
#   Egress=false, RuleNumber=100, Protocol=6(tcp), PortRange=1024-65535,  RuleAction=allow  <-- the gotcha

Default NACLs allow all traffic both ways, so a custom NACL is what you are auditing here. If you find an outbound allow but no inbound allow for 1024-65535, that is your bug. Remember NACL rules are evaluated in numbered order, lowest first, and the first match wins — a low-numbered DENY will shadow a higher-numbered ALLOW.

Diagram-style view of network traffic routing across a layered cloud topology
Outbound TCP crosses four independent gates on EC2: security group, NACL, route table, and the host firewall. Any one of them can silently drop the packet.

Layer 3: Does the subnet have a real path to the internet?

Even with both firewalls open, a packet needs somewhere to go. This is decided by the route table attached to the subnet, and it is the most common reason a 'private' EC2 instance cannot reach anything. A public subnet needs a route 0.0.0.0/0 pointing at an internet gateway (igw-) AND the instance needs a public or Elastic IP. A private subnet has no IGW route on purpose — it reaches the internet only through a NAT gateway, with a route 0.0.0.0/0 -> nat-xxxx. No NAT gateway means no outbound, full stop. I cover how this fits a hardened multi-tier setup in my walkthrough on deploying Laravel on AWS with a production architecture.

bash
# find the route table for your subnet and look at its default route
aws ec2 describe-route-tables \
  --filters Name=association.subnet-id,Values=subnet-0aa11bb22cc33 \
  --query 'RouteTables[].Routes'

# public subnet, healthy:  DestinationCidrBlock=0.0.0.0/0  GatewayId=igw-0f1e2d3c
# private subnet, healthy: DestinationCidrBlock=0.0.0.0/0  NatGatewayId=nat-09a8b7c6
# broken: no 0.0.0.0/0 entry at all  -> traffic has nowhere to go, hangs

One subtlety: if the subnet has no explicit route-table association, it falls back to the VPC's main route table, and the filter above can come back empty. In that case query the main route table directly. And for a public subnet, an IGW route with no public IP on the instance is still a dead end — the IGW has nothing to map your private address to.

Layer 4: Is the host's own firewall the problem?

If all three AWS layers check out, the block is on the box itself. ufw, firewalld, or a hand-rolled iptables OUTPUT chain can drop egress just as effectively as a security group, and AWS gives you no visibility into it. This bites hardest on golden AMIs and hardened CIS images where outbound filtering ships enabled by default. Note that neither Amazon Linux 2023 nor AL2 installs firewalld by default — AL2023 uses an nftables backend — so the iptables/nft dump below is your reliable ground truth.

bash
# ufw (Ubuntu/Debian)
sudo ufw status verbose

# firewalld (RHEL / CentOS Stream / Fedora family, when installed)
sudo firewall-cmd --list-all

# ground truth, regardless of front-end. on modern distros iptables is the
# nft compatibility shim, so also dump native nftables rules directly:
sudo iptables -S
sudo nft list ruleset
# look for a default DROP/REJECT policy on the OUTPUT chain, or an
# explicit rule that drops your destination port

The four-layer checklist (and the SMTP wrinkle)

  • Probe first: nc -zv -w 5 host port (or the /dev/tcp test). Hang/timeout = network block; refused = service problem, stop here.
  • Layer 1 — Security Group egress: stateful, so only the outbound rule matters. Confirm a rule covers your destination port.
  • Layer 2 — Network ACLs: stateless. Allow outbound AND inbound for ephemeral return ports 1024-65535. This is the classic silent dropper.
  • Layer 3 — Route table: public subnet needs 0.0.0.0/0 -> igw + a public IP; private subnet needs 0.0.0.0/0 -> nat-xxxx. No NAT, no outbound.
  • Layer 4 — Host firewall: sudo iptables -S, sudo nft list ruleset, sudo ufw status. Look for a DROP policy on OUTPUT.
  • SMTP wrinkle: AWS throttles outbound port 25 by default on all EC2. Use 587 or 465, or open a support case to lift the restriction.

That port 25 throttle is worth calling out because it produces the exact hang-and-timeout signature with every other layer wide open. EC2 silently rate-limits outbound 25 to fight spam, so a mail server trying to talk SMTP directly will appear blocked no matter how many egress rules you add. The right move is to send over the submission ports 587 or 465 to a relay. If you are running transactional mail, the cleaner fix is to hand it off to a managed sender entirely — I walk through the setup and the reputation side of it in my email deliverability guide for developers, since blocked SMTP egress is one of the most common reasons people think their mail provider is broken when it is really the network.

On EC2, a hang is a routing or firewall story; a refusal is a service story. Read the failure mode first and you have already eliminated half the suspects.Md Raihan Hasan

The path out of an EC2 instance is four independent gates — stateful SG, stateless NACL, route table, host firewall — and once you treat it that way the timeouts stop being a guessing game. Reproduce the symptom with a five-second TCP probe, then walk the gates in order with the describe commands above. In my experience it is usually a NACL missing the ephemeral return range or a private subnet with no NAT gateway, and you will have it pinned in a few minutes instead of restarting services and blaming the remote API.