Ssh connection refused on port 22 usually means the SSH daemon isn’t running, or a firewall is blocking inbound traffic. Check if sshd is active via Systemd. Verify iptables or UFW rules. Test TCP connectivity with Netcat. Ensure your public key authentication config in sshd_config is not rejecting your IP. Fixing this requires isolating whether the issue is service-level or network-level before tweaking advanced security policies.
Most engineers mix these up. In my experience supporting legacy Linux infra, the error message tells you exactly where to look. If you see “Connection refused,” the packet reached the host. The OS said “no.” The process listening on that port might be dead, or a local firewall rule explicitly dropped it.
If you see “Connection timed out,” the packet never got answered. The host is likely down, or an external firewall dropped the packet silently. No response. Just silence. For this guide, we focus strictly on the “refused” state. It implies the server is alive but rejecting your doorbell. Treat timeouts as a separate network outage problem, not an SSH configuration issue.
The SSH daemon depends on the system’s network stack and configuration files. A typo in one six-line block can kill the entire service. When systemd stops sshd, port 22 closes immediately. Clients receive the RST (Reset) packet instantly.
Common culprits I see in production support tickets:
/etc/ssh/ssh_host_* files are missing or corrupt, sshd refuses to start. This often happens after a bad disk write or a partial file copy.AddressFamily inet while your server only has IPv6 configured. The daemon tries to bind to a non-existent socket.Check the logs first. Always. Run journalctl -u ssh -n 50 to see the last 50 lines of the service log. Look for “failed to load host key” or “address already in use.” If the log shows a clean startup but the port is closed, the issue is likely a firewall rule overriding the socket state.
Firewalls operate in layers. Even if sshd is running, iptables can block the traffic before it hits the socket. This is where many “refused” errors actually originate. The host is up, the service is up, but the gate is locked.
On Debian and Ubuntu systems, UFW manages these rules. Check your status immediately. Run sudo ufw status verbose. Look for line 22 (or 322 if you moved it). It should say ALLOW. If it says REJECT or DROP, you have found your problem. Add the rule with sudo ufw allow 22/tcp.
On CentOS or RHEL, firewalld handles this differently. You need sudo firewall-cmd --list-ports. If 22/tcp isn’t there, add it with sudo firewall-cmd --add-port=22/tcp --permanent and reload. I’ve seen cases where a reboot reset the temporary rules, causing sudden outages. Always make rules permanent if you want them to survive a power cycle.
Assuming the firewall is clean, we need to confirm the daemon is actually bound to the interface. Use Netcat for a raw TCP check. This bypasses the SSH handshake entirely. It just tests if the port is open.
Run this from your local machine:
nc -zv your_server_ip 22
If you get “Connection succeeded,” the port is open. The issue is now strictly within the SSH protocol negotiation, not the connectivity layer. If you get “Connection refused,” the port is closed despite your beliefs. There is a discrepancy between what you think is running and what is actually bound to the network card.
On the server, verify the binding: ss -tlnp | grep :22. This shows all listening TCP sockets. If sshd appears in the process column, it is listening. If nothing appears, the service isn’t running. Start it with sudo systemctl start ssh. Check the status again. If it fails, read the error message in the output. It is specific. Trust it.
Sometimes the connection establishes, but the protocol handshake fails. Or, you get refused because sshd is configured to reject unknown IPs before even offering the password prompt. This is a security feature that becomes a usability nightmare if misconfigured.
Check /etc/ssh/sshd_config. Look for AllowUsers or AllowGroups. If these lines exist, they override default access policies. If your username isn’t in that list, the connection is rejected instantly. I recall a ticket where a developer added AllowUsers deploy but forgot to add his own account to the deploy group. He couldn’t log in for two hours. Not fun.
Also inspect HostbasedAuthentication and PubkeyAuthentication. If both are set to no, and you don’t have a password entry, you are locked out. In 2026, disabling password authentication is standard best practice, per OWASP guidelines on secure authentication. But you must ensure your key-based auth is working *before* you flip the switch. Test with a fallback SSH session first.
Modern servers often have multiple network interfaces. The ListenAddress directive in sshd_config tells the daemon where to bind. If you specify ListenAddress 192.168.1.10 but your server is currently at 10.0.0.5, the bind fails. The service crashes or fails to start.
Check your actual IP with ip addr show. Compare it to your sshd_config. If they differ, remove the ListenAddress line entirely. By default, sshd listens on all available interfaces (0.0.0.0 or ::). This is usually what you want. Explicit binding is only necessary for complex multi-homed server setups.
Also, check AddressFamily. If set to inet (IPv4 only) and you are connecting via IPv6, you get refused. If set to ipv6 and you lack IPv6 support, same result. The safest setting is any or simply commenting the line out to let the system decide. Review your config file for implicit assumptions about your network state.
If standard checks fail, enable verbose logging. This increases file size and CPU usage slightly, but it captures every detail. In production, use a temporary file instead of growing /var/log/auth.log indefinitely.
Run sudo sshd -d -p 2222. This creates a debug instance on port 2222. It runs in the foreground. All logs appear in your terminal. Now, connect to port 2222 from your client with ssh -v user@ip -p 2222. The -v flag enables verbose client output. You will see exactly where the handshake breaks. Is it at key exchange? Is it at user authentication? The logs side-by-side will reveal the exact rejection reason.
Common hidden issues found in debug mode:
Once identified, update your sshd_config to match the supported crypto parameters. Restart the service. Verify the connection. Don’t forget to kill the debug instance on 2222 when you are done.
Prevention beats recovery. Build a monitoring habit. Use Zabbix or Prometheus to monitor port 22 availability. If the port closes, alert immediately. Don’t wait for a user to complain. You lose an hour of downtime before you even know something is wrong.
Manage changes through configuration management tools. Ansible or Chef scripts ensure sshd_config remains consistent across your fleet. Manual edits drift. Drift causes outages. I’ve seen servers where someone manually commented out a line, forgot to document it, and caused a cascade of failures months later. Automate where possible.
Keep your SSH keys organized. Use dedicated key pairs per admin. Revoke compromised keys immediately. Rotating keys every 90 days reduces the blast radius of a leak. Also, consider moving SSH to a non-standard port *after* securing it properly. This lowers low-effort scanning noise, but it does not replace hardening. Security through obscurity is weak. Hardening is strong.
Familiarize yourself with your server’s recovery process. If remote access fails, do you have console access via your hosting provider? If the answer is no, you have a single point of failure. Learn how to access the IPMI or vKVM interface of your server. It is boring practice until the day you need it. Then it is a lifesaver.
Usually, temporary firewall rules were lost. UFW and firewalld often reset to default deny if not saved as permanent. Also, check if the sshd service failed to start due to a corrupted config file edited before the reboot. Check journalctl for startup errors.
Yes. Change Port 22 to Port 2222 in sshd_config, restart the service, and test. If 2222 works, the issue is specific to port 22 restrictions or a conflicting application using port 22. This isolates the variable effectively.
Yes. Cloudflare’s standard proxy does not support raw TCP traffic for SSH. You must use “Grey Cloud” (DNS only) mode or enable Flexible SSH (if available on your plan) to route port 22 traffic directly to your origin server. Proxying SSH causes handshake failures.
Run sestatus to check if SELinux is enforcing. If it is, check the audit log for DENIED messages related to sshd_t. Use ausearch -m avc -ts recent to see recent denials. Fix the context with restorecon or adjust the policy.