Your SSH client sits there for thirty seconds, then prints “ssh: connect to host 1.2.3.4 port 22: Operation timed out”, and you’re no closer to knowing why. Debugging an ssh connection timeout on an AWS EC2 instance is frustrating precisely because the error tells you almost nothing: no response arrived, full stop. It doesn’t say whether the instance is down, a security group is blocking you, a network ACL is silently dropping your return traffic, or your local IP changed an hour ago. This guide walks through the checks in the order that actually finds the cause fastest, not the order AWS’s own docs happen to list them in.
Why an ssh connection timeout is not the same problem as “connection refused” or “permission denied”
Before touching security groups, confirm which error you actually have, because the fix is completely different for each one.
- A timeout means no packet ever came back: your traffic is being dropped somewhere on the path, or the instance never received it.
- “Connection refused” means a packet did arrive and something, usually nothing listening on that port, actively rejected it.
- “Permission denied (publickey)” means the connection worked fine and you’re now failing authentication: a key or credentials problem, not a network one.
Mixing these up wastes real time, chasing route tables when the actual problem is an expired key, or the reverse. If your error is specifically about keys or credentials rather than a hang, see how end-to-end encrypted SSH credential storage works instead of the network checklist below.
How to fix an ssh connection timeout on AWS EC2, step by step
Work through these in order, cheapest and most likely first. Most timeouts get resolved in the first two steps, before you ever have to open the VPC console.
Step 1: confirm the instance is actually running
Open the EC2 console and check the instance state and status checks, not just that it says “running”. An instance can show as running while still failing initialization after a resize, a kernel panic, or a stuck boot volume. Both the instance status check and the system status check need to show 2/2 passed before SSH has any chance of working. If a status check is failing, that’s your root cause, not the network.
Step 2: confirm you’re using the instance’s current public IP
Unless it has an Elastic IP attached, an EC2 instance gets a new public IP every time it stops and starts. A saved SSH config entry, a script, or a bookmark pointing at yesterday’s IP will time out against an address that now belongs to a different instance, or to nothing at all. Pull the current public IPv4 address from the console, or `aws ec2 describe-instances`, before assuming the network is broken.
Is your security group blocking inbound port 22?
This is the most common cause, and the fix is a single rule. Open the security group attached to the instance’s network interface and confirm it has an inbound rule allowing TCP port 22 from your current IP: not last week’s IP, not 0.0.0.0/0 left over from a tutorial, and not scoped to a corporate VPN range you’re no longer on.
Type: SSH
Protocol: TCP
Port range: 22
Source: <your-current-public-ip>/32Security groups are stateful: once inbound traffic is allowed, the matching outbound response is allowed automatically. That’s what keeps this check simple, and it’s exactly why the next layer catches people off guard.
Are your NACLs the reason, even though the security group looks fine?
Network ACLs operate at the subnet level, sit in front of the security group, and are stateless: they evaluate inbound and outbound traffic as two completely separate rule sets. Allowing inbound port 22 on the NACL is not enough. The instance’s reply also needs an explicit outbound rule covering the client’s ephemeral port range, usually 1024-65535, or the response never leaves the subnet and your client just waits until it gives up. This is the single most-missed step in generic troubleshooting checklists, because a security group that looks fine and a NACL that looks fine can both be true while the ephemeral-port rule is the one thing missing.
Rule #: 100
Type: Custom TCP
Port range: 1024-65535
Source: 0.0.0.0/0
Allow/Deny: ALLOW (outbound)Does your route table actually reach an internet gateway?
If the instance sits in a public subnet, that subnet’s route table needs a route sending 0.0.0.0/0 traffic to an internet gateway (igw-…), not just the VPC’s local route. Without it, the instance can have a public IP assigned and still be completely unreachable from outside the VPC, and every security group and NACL rule you’ve checked will look correct, because none of them were the problem.
Prove where the packet is dying with ssh -vvv and nc
Once the obvious checks are done, stop guessing and get the client and a raw TCP probe to tell you directly where the connection fails.
ssh -vvv user@<public-ip>Look at the last line printed before it hangs. If it stalls on “Connecting to port 22”, nothing ever responded at the transport layer, which points at the network path, security group, NACL, route table, or a stopped instance, rather than SSH itself.
nc -zv <public-ip> 22 -w 5A succeeded result means TCP itself connected fine, and any remaining problem is inside SSH configuration or authentication, not the network. A timeout here confirms the network path is still the problem, and sends you back to the security group, NACL, and route table checks above, not forward to key or config debugging.
Security groups, NACLs, and routes all check out. Now what?
If every network layer is provably correct via the nc test and the instance still won’t accept an SSH session, the remaining suspects live on the instance itself: an OS-level firewall, iptables, ufw, or firewalld, blocking port 22 locally, or the sshd service not actually running. You can’t SSH in to check either of those, so use a side channel that doesn’t depend on port 22 at all.
Skip the network layer entirely: EC2 Instance Connect and Session Manager
EC2 Instance Connect, browser-based, from the console’s connect button, and AWS Systems Manager Session Manager, agent-based, working even with no open inbound ports at all, both give you a shell on the instance over the AWS API instead of a direct TCP connection to port 22. Use either one to check sshd’s status and the local firewall rules directly, confirm they’re the problem, fix them from inside, then go back to your normal SSH client afterward.
When the timeout isn’t AWS’s fault at all
The checklist above assumes the problem lives inside the VPC, but a real share of these timeouts trace back to the network you’re connecting from, not the one you’re connecting to. A few causes worth ruling out before you touch anything in the AWS console again:
- Office wifi, a corporate VPN, or a hotel network that blocks outbound port 22 entirely. Test from a phone hotspot: if it connects instantly from there, the block is local, not AWS’s.
- A split-tunnel VPN routing your traffic through a path that never reaches the internet gateway your instance expects, even though the same VPN works fine for everything else.
- A DNS record resolving to an IPv6 address the instance doesn’t actually have a working route for, so the client tries AAAA first and stalls before ever falling back to the IPv4 address.
- A jump host or bastion in the middle: if you’re chaining through `ProxyJump`, the timeout might belong to the bastion’s own security group or NACL, not the final target’s.
None of these show up in the EC2 console, because from AWS’s side the request never arrived at all. `ssh -vvv` and `nc` from a different network are the fastest way to tell a client-side block apart from a genuine AWS misconfiguration.
The order to check, as a quick reference
- Instance state and status checks (2/2 passed)
- Current public IP, not a stale saved one
- Security group: inbound TCP 22 from your real current IP
- NACL: inbound 22 AND outbound 1024-65535 both allowed
- Route table: 0.0.0.0/0 pointed at an internet gateway
- ssh -vvv and nc -zv to see exactly where it stalls
- OS firewall and sshd status, checked via Instance Connect or Session Manager
- Your own network, VPN, DNS, or bastion, if AWS-side checks are all clean
Stop re-debugging the same host every time
Every one of these checks takes a few minutes in isolation, but the real cost on a growing fleet of EC2 instances is doing them from memory, per host, every time an IP rotates or a security group changes. A purpose-built SSH client that groups and tags hosts means you’re not reconstructing which instance had which fix from shell history six weeks later, and a per-command audit log gives you a real record of what was actually tried on a shared host instead of a thread full of guesses. For the wider picture of what else belongs in a hardened setup beyond this one failure mode, see the complete guide to SSH security.
termique is a free ssh manager that groups your EC2 hosts, keeps a per-command audit log for anything run through it, and never stores your credentials in plaintext. It won’t fix a misconfigured security group, but it turns the next timeout into a five-minute check instead of a fresh investigation.
