{"id":723,"date":"2026-07-03T07:27:04","date_gmt":"2026-07-03T07:27:04","guid":{"rendered":"https:\/\/blog-origin.donely.ai\/blog\/ssh-port-22-connection-refused\/"},"modified":"2026-07-03T07:27:07","modified_gmt":"2026-07-03T07:27:07","slug":"ssh-port-22-connection-refused","status":"publish","type":"post","link":"https:\/\/blog-origin.donely.ai\/blog\/ssh-port-22-connection-refused\/","title":{"rendered":"SSH Port 22 Connection Refused: A Fast Fix Guide"},"content":{"rendered":"<p>You&#039;re probably here because a deploy worked yesterday, your VM is still up, and now your terminal says <strong>ssh: connect to host \u2026 port 22: Connection refused<\/strong>. That message feels simple, but it&#039;s useful. It usually means the server answered your TCP knock and said nothing is listening there, or something on the path rejected the connection fast enough that you didn&#039;t get a timeout.<\/p>\n<p>That&#039;s good news. A refusal is usually easier to debug than silence. For teams running cloud VMs, containers, and headless services like AI agents, this problem often comes from a short list of mistakes: SSH never started after a package update, the service moved to a custom port, or a cloud rule and host rule drifted apart after initial setup.<\/p>\n<h2>Table of Contents<\/h2>\n<ul>\n<li><a href=\"#the-three-most-likely-culprits\">The Three Most Likely Culprits<\/a><ul>\n<li><a href=\"#what-each-culprit-looks-like\">What each culprit looks like<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#your-first-responder-quick-checks\">Your First Responder Quick Checks<\/a><ul>\n<li><a href=\"#start-with-reachability\">Start with reachability<\/a><\/li>\n<li><a href=\"#confirm-you-didnt-aim-at-the-wrong-thing\">Confirm you didn&#039;t aim at the wrong thing<\/a><\/li>\n<li><a href=\"#know-when-to-stop-testing-from-the-client\">Know when to stop testing from the client<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#is-sshd-running-and-listening-correctly\">Is SSHD Running and Listening Correctly<\/a><ul>\n<li><a href=\"#check-the-service-before-the-config\">Check the service before the config<\/a><\/li>\n<li><a href=\"#running-is-not-the-same-as-listening\">Running is not the same as listening<\/a><\/li>\n<li><a href=\"#validate-the-config-before-you-restart-again\">Validate the config before you restart again<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#unblocking-the-path-firewalls-and-security-groups\">Unblocking the Path Firewalls and Security Groups<\/a><ul>\n<li><a href=\"#the-host-firewall-layer\">The host firewall layer<\/a><\/li>\n<li><a href=\"#the-cloud-firewall-layer\">The cloud firewall layer<\/a><\/li>\n<li><a href=\"#when-both-look-open-but-ssh-still-fails\">When both look open but SSH still fails<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#advanced-scenarios-containers-and-configuration-traps\">Advanced Scenarios Containers and Configuration Traps<\/a><ul>\n<li><a href=\"#the-host-port-is-not-always-the-container-port\">The host port is not always the container port<\/a><\/li>\n<li><a href=\"#selinux-and-policy-mismatches\">SELinux and policy mismatches<\/a><\/li>\n<li><a href=\"#small-config-mistakes-that-stop-everything\">Small config mistakes that stop everything<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#prevention-and-hardening-your-ssh-setup\">Prevention and Hardening Your SSH Setup<\/a><ul>\n<li><a href=\"#fix-the-outage-then-reduce-the-blast-radius\">Fix the outage, then reduce the blast radius<\/a><\/li>\n<li><a href=\"#build-a-safer-default-ssh-posture\">Build a safer default SSH posture<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><a id=\"the-three-most-likely-culprits\"><\/a><\/p>\n<h2>The Three Most Likely Culprits<\/h2>\n<p>Treat <strong>SSH port 22 connection refused<\/strong> as a narrowing signal, not a mystery. In practice, it almost always lands in one of three buckets: you&#039;re hitting the wrong host, you&#039;re hitting the right host on the wrong port, or you&#039;re hitting the right host and port but nothing is listening.<\/p>\n<p>That framing matters because it keeps you from editing <code>sshd_config<\/code> too early. The most reliable troubleshooting order is to work backward from the client side, then move inward to the server. The Raspberry Pi forum summary still offers a clear breakdown of potential issues: <strong>wrong IP address, correct IP but wrong port, or correct IP and port but server not listening<\/strong>. It also notes that this reverse-order path works well because it separates network problems from application problems <a href=\"https:\/\/forums.raspberrypi.com\/viewtopic.php?t=256251\">in this diagnostic breakdown<\/a>.<\/p>\n<p><a id=\"what-each-culprit-looks-like\"><\/a><\/p>\n<h3>What each culprit looks like<\/h3>\n<ul>\n<li><strong>Wrong address:<\/strong> You rebuilt a VM, the public endpoint changed, DNS lagged, or you copied the private address from a cloud console and tried it from outside the network.<\/li>\n<li><strong>Wrong port:<\/strong> The server is healthy, but SSH was moved to <code>2222<\/code> or another custom port and your client still tries <code>22<\/code>.<\/li>\n<li><strong>Not listening:<\/strong> <code>sshd<\/code> is stopped, failed at boot, or started with a config that prevents it from binding where you expect.<\/li>\n<\/ul>\n<blockquote>\n<p><strong>Practical rule:<\/strong> \u201cConnection refused\u201d is usually a fast no. That&#039;s different from a timeout, which usually means packets are getting dropped somewhere.<\/p>\n<\/blockquote>\n<p>Junior admins often lose time because they assume a refusal proves the network is fine. It doesn&#039;t. It proves only that you got a decisive answer. A firewall, proxy, or misrouted path can still produce a fast rejection. Start with the simplest checks, confirm the target, then prove whether the daemon is listening.<\/p>\n<p><a id=\"your-first-responder-quick-checks\"><\/a><\/p>\n<h2>Your First Responder Quick Checks<\/h2>\n<p>Before you open a cloud console or touch the server, do a <strong>60-second client-side triage<\/strong>. The goal is to answer one question: are you chasing the right machine on the right port?<\/p>\n<p><figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog-origin.donely.ai\/wp-content\/uploads\/2026\/07\/ssh-port-22-connection-refused-ethernet-connection.jpg\" alt=\"A close-up view of a person hand connecting an ethernet cable to a laptop computer port.\" \/><\/figure><\/p>\n<p><a id=\"start-with-reachability\"><\/a><\/p>\n<h3>Start with reachability<\/h3>\n<p>Run these from your local machine:<\/p>\n<pre><code class=\"language-bash\">ping -c 4 your-hostname\nnc -zv your-hostname 22\ntelnet your-hostname 22\n<\/code><\/pre>\n<p><code>ping<\/code> is only a rough signal. Some environments block ICMP and still allow SSH, so don&#039;t treat a failed ping as final. But if <code>ping<\/code> works and <code>nc<\/code> or <code>telnet<\/code> gives you an immediate refusal, you&#039;ve learned something useful: the host is reachable, but port 22 isn&#039;t accepting sessions.<\/p>\n<p>The sharpest first-pass test is <code>nc -zv<\/code>. The verified guidance says <strong><code>nc -zv server.example.com 22<\/code> returns \u201cConnection refused\u201d in 100% of cases where the daemon is not running<\/strong>, which makes it a clean yes-or-no probe before you even get console access <a href=\"https:\/\/oneuptime.com\/blog\/post\/2026-03-04-fix-connection-refused-port-22-ssh-rhel\/view\">from this RHEL troubleshooting guide<\/a>.<\/p>\n<p><a id=\"confirm-you-didnt-aim-at-the-wrong-thing\"><\/a><\/p>\n<h3>Confirm you didn&#039;t aim at the wrong thing<\/h3>\n<p>If you&#039;re working in AWS, Azure, GCP, DigitalOcean, or a lab with multiple headless nodes, double-check the target details before assuming the service is broken.<\/p>\n<p>Use a simple checklist:<\/p>\n<ol>\n<li><strong>Hostname check:<\/strong> Does the hostname still resolve to the instance you expect?<\/li>\n<li><strong>Recent rebuilds:<\/strong> Did someone replace the VM, restore from snapshot, or rotate networking?<\/li>\n<li><strong>Port assumption:<\/strong> Are you sure the service still uses <code>22<\/code>?<\/li>\n<\/ol>\n<p>A lot of modern setups, especially container-heavy and bot-exposed systems, move SSH to another port and forget to document it. That&#039;s common in solo projects and internal tools because the person who changed it remembers. Everyone else doesn&#039;t.<\/p>\n<blockquote>\n<p>If <code>telnet your-host 22<\/code> refuses immediately, focus on listening services and port selection before you start debugging keys, users, or auth methods.<\/p>\n<\/blockquote>\n<p><a id=\"know-when-to-stop-testing-from-the-client\"><\/a><\/p>\n<h3>Know when to stop testing from the client<\/h3>\n<p>Client-side checks are for <strong>direction<\/strong>, not complete diagnosis. If the endpoint is reachable and port 22 refuses consistently, stop guessing from your laptop. Move to the server console, serial console, provider recovery shell, or another working session.<\/p>\n<p>From there, the question becomes straightforward: is <code>sshd<\/code> alive, and if it is, where is it listening?<\/p>\n<p><a id=\"is-sshd-running-and-listening-correctly\"><\/a><\/p>\n<h2>Is SSHD Running and Listening Correctly<\/h2>\n<p>Most outages end here. The refusal often isn&#039;t exotic. It&#039;s just <code>sshd<\/code> stopped, failed, or bound somewhere other than the port you expected.<\/p>\n<p><figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog-origin.donely.ai\/wp-content\/uploads\/2026\/07\/ssh-port-22-connection-refused-ssh-diagnostics.jpg\" alt=\"A three-step infographic titled SSH Daemon Diagnostics illustrating how to troubleshoot server connectivity issues effectively.\" \/><\/figure><\/p>\n<p><a id=\"check-the-service-before-the-config\"><\/a><\/p>\n<h3>Check the service before the config<\/h3>\n<p>Get onto the machine through a console and ask the service manager first:<\/p>\n<pre><code class=\"language-bash\">sudo systemctl status sshd\nsudo systemctl start sshd\nsudo systemctl enable sshd\n<\/code><\/pre>\n<p>On some distributions the service name is <code>ssh<\/code> instead of <code>sshd<\/code>, so check both if needed.<\/p>\n<p>Red Hat&#039;s verified guidance is blunt here: <strong>a connection refused on SSH port 22 indicates that <code>sshd<\/code> is not running or not listening on the target port<\/strong>. It also says that <strong>in 99.9% of documented cases, the root cause is a stopped service, a startup failure caused by config errors, or a non-standard listening port<\/strong> <a href=\"https:\/\/access.redhat.com\/solutions\/7102987\">according to Red Hat&#039;s troubleshooting note<\/a>.<\/p>\n<p>That lines up with what many admins see in real environments. Especially on unattended systems, package updates, image changes, or a bad config edit can leave <code>sshd<\/code> inactive after reboot.<\/p>\n<p><a id=\"running-is-not-the-same-as-listening\"><\/a><\/p>\n<h3>Running is not the same as listening<\/h3>\n<p>A green service state doesn&#039;t fully prove the endpoint is reachable. You also need to see the socket:<\/p>\n<pre><code class=\"language-bash\">sudo ss -lnp | grep 22\nsudo ss -tlnp | grep sshd\n<\/code><\/pre>\n<p>Look for a listening socket on the expected interface and port. If you see <code>2222<\/code> instead of <code>22<\/code>, the service is up, but your client command is wrong. If you see it bound only to localhost or a narrow <code>ListenAddress<\/code>, remote clients won&#039;t get in.<\/p>\n<p>If you want another way to inspect sockets, a good refresher is this walkthrough on how to <a href=\"https:\/\/uptimewebhosting.com.au\/website-hosting\/netstat-a-command\/\">check network ports with netstat -a<\/a>. It&#039;s useful when you&#039;re on older systems or cross-checking what <code>ss<\/code> shows.<\/p>\n<p>Here&#039;s the distinction that trips people up:<\/p>\n\n<figure class=\"wp-block-table\"><table><tr>\n<th>State<\/th>\n<th>What it means<\/th>\n<th>What to do<\/th>\n<\/tr>\n<tr>\n<td><code>systemctl<\/code> says inactive<\/td>\n<td>SSH daemon isn&#039;t running<\/td>\n<td>Start it and inspect logs<\/td>\n<\/tr>\n<tr>\n<td>Service is active, no port 22 listener<\/td>\n<td>SSH is up incorrectly or on another port<\/td>\n<td>Check <code>Port<\/code> and <code>ListenAddress<\/code><\/td>\n<\/tr>\n<tr>\n<td>Listener exists on another port<\/td>\n<td>Port mismatch<\/td>\n<td>Connect with <code>ssh -p ...<\/code><\/td>\n<\/tr>\n<tr>\n<td>Listener exists on 22<\/td>\n<td>Service side may be fine<\/td>\n<td>Check firewall layers next<\/td>\n<\/tr>\n<\/table><\/figure>\n<p><a id=\"validate-the-config-before-you-restart-again\"><\/a><\/p>\n<h3>Validate the config before you restart again<\/h3>\n<p>Bad SSH config edits cause avoidable outages. Validate before bouncing the daemon:<\/p>\n<pre><code class=\"language-bash\">sudo sshd -t\nsudo grep -E &quot;^Port|^ListenAddress|^AllowUsers|^AllowGroups|^DenyUsers|^DenyGroups|^Match&quot; \/etc\/ssh\/sshd_config\n<\/code><\/pre>\n<p>Common breakpoints include:<\/p>\n<ul>\n<li><strong>Custom port drift:<\/strong> <code>Port 2222<\/code> is set, but docs and scripts still use <code>22<\/code>.<\/li>\n<li><strong>Restrictive bind:<\/strong> <code>ListenAddress<\/code> points at the wrong interface.<\/li>\n<li><strong>Access rules:<\/strong> <code>AllowUsers<\/code> or <code>AllowGroups<\/code> excludes the account you&#039;re trying to use.<\/li>\n<li><strong>Third-party edits:<\/strong> automation tools or security packages changed ciphers or options in ways your OpenSSH build won&#039;t accept.<\/li>\n<\/ul>\n<p>Later-stage logs tell you why a restart failed. Use:<\/p>\n<pre><code class=\"language-bash\">sudo journalctl -u sshd\n<\/code><\/pre>\n<p>If the daemon failed to start, Red Hat notes that <code>journalctl -u sshd<\/code> will show <code>error<\/code> or <code>fail<\/code> messages within a short diagnostic window after the failure, which makes logs the fastest way to stop guessing.<\/p>\n<p>A short video walkthrough can help if you&#039;re doing this under pressure and want to compare your screen to a known-good flow:<\/p>\n<iframe width=\"100%\" style=\"aspect-ratio: 16 \/ 9\" src=\"https:\/\/www.youtube.com\/embed\/W4OKgiuIsEw\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen><\/iframe>\n\n<blockquote>\n<p>A healthy SSH setup has three layers in agreement: the service is running, the config is valid, and the socket is listening where the client expects.<\/p>\n<\/blockquote>\n<p><a id=\"unblocking-the-path-firewalls-and-security-groups\"><\/a><\/p>\n<h2>Unblocking the Path Firewalls and Security Groups<\/h2>\n<p>You can have a healthy <code>sshd<\/code> process and still get locked out if the packet never reaches it. That happens all the time on cloud VMs, forgotten AI worker hosts, and boxes that were hardened once and then left alone for months. After you confirm the daemon is listening, treat the network path as the problem until you prove otherwise.<\/p>\n<p><figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog-origin.donely.ai\/wp-content\/uploads\/2026\/07\/ssh-port-22-connection-refused-firewall-troubleshooting.jpg\" alt=\"A flowchart diagram explaining the two main layers of firewall troubleshooting for blocking SSH access.\" \/><\/figure><\/p>\n<p><a id=\"the-host-firewall-layer\"><\/a><\/p>\n<h3>The host firewall layer<\/h3>\n<p>Start on the server. The goal is simple: confirm the OS is willing to accept TCP traffic on the port where <code>sshd<\/code> is listening.<\/p>\n<p>On Ubuntu and Debian, check UFW first:<\/p>\n<pre><code class=\"language-bash\">sudo ufw status verbose\nsudo ufw allow ssh\n<\/code><\/pre>\n<p><code>ufw status verbose<\/code> shows whether UFW is active and whether a rule already permits SSH. <code>ufw allow ssh<\/code> opens the default SSH service definition, which usually maps to TCP 22. If your daemon listens on another port, allow that port directly instead of assuming the named service still matches your setup.<\/p>\n<p>On RHEL, CentOS, AlmaLinux, or Rocky, check <code>firewalld<\/code>:<\/p>\n<pre><code class=\"language-bash\">sudo firewall-cmd --list-services\nsudo firewall-cmd --add-service=ssh --permanent\nsudo firewall-cmd --reload\n<\/code><\/pre>\n<p><code>--list-services<\/code> shows what the active zone currently permits. <code>--add-service=ssh --permanent<\/code> writes the rule so it survives reboot. <code>--reload<\/code> applies it without making you restart the machine. If SSH runs on 2222 or any other custom port, use a port rule instead of the <code>ssh<\/code> service entry.<\/p>\n<p>If the host uses raw <code>iptables<\/code> or <code>nftables<\/code>, inspect the input chain for <code>DROP<\/code> or <code>REJECT<\/code> rules that hit your SSH port. A reject usually returns fast and looks like an immediate refusal. A drop often looks more like a timeout. That distinction saves time.<\/p>\n<p><a id=\"the-cloud-firewall-layer\"><\/a><\/p>\n<h3>The cloud firewall layer<\/h3>\n<p>Cloud policy is a separate gate. AWS Security Groups, Azure Network Security Groups, GCP firewall rules, and similar controls decide whether the packet even reaches the VM.<\/p>\n<p>Check the platform control that applies to your instance:<\/p>\n<ul>\n<li><strong>AWS:<\/strong> Security Groups, and sometimes Network ACLs<\/li>\n<li><strong>Azure:<\/strong> Network Security Groups<\/li>\n<li><strong>GCP:<\/strong> VPC firewall rules<\/li>\n<li><strong>DigitalOcean and similar platforms:<\/strong> provider firewall controls<\/li>\n<\/ul>\n<p>This layer gets missed on headless systems. A team stands up a VM for an AI agent, background worker, or internal automation job, confirms SSH once, and stops thinking about it. Months later someone clones the instance, tightens inbound policy, swaps subnets, or rebuilds from an image that never had the right rule. The workload still runs. Admin access disappears.<\/p>\n<p>I see this a lot with agent hosts because nobody logs in until something breaks. If you are comparing self-managed boxes with managed options for isolated agent workloads, platforms built for <a href=\"https:\/\/donely.ai\/openclaw-hosting\">OpenClaw hosting<\/a> reduce the number of firewall layers you have to remember and document by hand.<\/p>\n<p>For teams that also expose services through home labs, office routers, or gateway firewalls, this <a href=\"https:\/\/monrocloud.com\/it-security\/how-to-set-up-port-forwarding\/\">practical guide to port forwarding<\/a> is useful because it explains how traffic is translated before it reaches the host. The same model applies when you troubleshoot NAT, bastion hosts, and private subnets.<\/p>\n<p><a id=\"when-both-look-open-but-ssh-still-fails\"><\/a><\/p>\n<h3>When both look open but SSH still fails<\/h3>\n<p>Work the path in order. Do not guess.<\/p>\n<ul>\n<li><strong>Listener present, host firewall closed:<\/strong> open the Linux firewall rule.<\/li>\n<li><strong>Listener present, cloud rule closed:<\/strong> fix the provider-side rule.<\/li>\n<li><strong>Both open, still refused:<\/strong> verify you are hitting the correct IP and the correct port.<\/li>\n<li><strong>Custom port in use:<\/strong> make sure every layer allows the same port, including any jump host, VPN policy, or edge firewall.<\/li>\n<\/ul>\n<p>One more practical check helps here. Test from a nearby machine in the same VPC, subnet, or private network segment. If SSH works internally but not from your laptop, the instance is usually fine and the problem sits at the cloud edge, office firewall, VPN, or port-forwarding layer.<\/p>\n<blockquote>\n<p>SSH access only works when the service, the host firewall, and the upstream network policy all agree on the same destination port.<\/p>\n<\/blockquote>\n<p>If the rule set looks right and the result still makes no sense, capture traffic. <code>tcpdump<\/code> on the server will tell you whether SYN packets arrive and whether the host sends a reset back. That is how you separate a real network block from a policy mistake that only looks like one.<\/p>\n<p><a id=\"advanced-scenarios-containers-and-configuration-traps\"><\/a><\/p>\n<h2>Advanced Scenarios Containers and Configuration Traps<\/h2>\n<p>When the normal checks don&#039;t solve it, the problem usually comes from abstraction. Containers, custom ports, security policies, and tiny config mistakes create failures that look simple from the client side and messy everywhere else.<\/p>\n<p><a id=\"the-host-port-is-not-always-the-container-port\"><\/a><\/p>\n<h3>The host port is not always the container port<\/h3>\n<p>A common example is Docker or Podman. The container runs SSH on port 22 internally, but the host publishes it on another port:<\/p>\n<pre><code class=\"language-bash\">docker run -p 2222:22 ...\n<\/code><\/pre>\n<p>From outside, <code>ssh user@host<\/code> fails on 22 and the admin assumes the daemon is dead. It isn&#039;t. The right command is <code>ssh -p 2222 user@host<\/code>.<\/p>\n<p>That confusion is growing, especially in headless automation and AI environments where people want to reduce noise from bots. Verified data says <strong>32% of new DevOps setups use custom ports to avoid bot attacks<\/strong>, and many troubleshooting guides still act like port 22 is always the only answer <a href=\"https:\/\/www.hostaccent.com\/blog\/ssh-connection-refused\">in this discussion of non-standard SSH ports<\/a>. That&#039;s why a port mismatch should stay high on your list.<\/p>\n<p>For teams hosting agent workloads, this gets worse when a reverse proxy, sidecar, or orchestrator owns the host networking and the container only sees internal ports. In those environments, document the published host port, not just the container default. If you&#039;re evaluating managed agent infrastructure, the operational trade-offs are easier to see in a platform-oriented setup like <a href=\"https:\/\/donely.ai\/hermes-agent\/hosting\">Hermes agent hosting<\/a>.<\/p>\n<p><a id=\"selinux-and-policy-mismatches\"><\/a><\/p>\n<h3>SELinux and policy mismatches<\/h3>\n<p>RHEL-family systems add another twist. You can open the firewall and still fail if SELinux doesn&#039;t allow SSH on the custom port you chose.<\/p>\n<p>The symptom is frustrating because it looks like a normal port problem. The process may even appear healthy, but policy stops the expected behavior. If you moved SSH off 22, inspect the allowed SSH port contexts and add the new port with <code>semanage<\/code> if needed.<\/p>\n<p><a id=\"small-config-mistakes-that-stop-everything\"><\/a><\/p>\n<h3>Small config mistakes that stop everything<\/h3>\n<p>The ugliest SSH outages often come from tiny text edits:<\/p>\n<ul>\n<li><code>AllowUsers<\/code> includes the wrong username<\/li>\n<li><code>ListenAddress<\/code> binds the daemon too narrowly<\/li>\n<li>A bad <code>Match<\/code> block changes behavior for one group or source<\/li>\n<li>Third-party tooling rewrites options and leaves <code>sshd<\/code> unable to start cleanly<\/li>\n<\/ul>\n<p>One habit prevents a lot of pain: test the config before restart, and keep a second console open while you reload. On remote systems, never bet your only session on an unvalidated SSH change.<\/p>\n<blockquote>\n<p>Most \u201cmystery\u201d SSH refusals in modern stacks aren&#039;t mysteries. They&#039;re translation problems between host ports, container ports, policy layers, and assumptions.<\/p>\n<\/blockquote>\n<p><a id=\"prevention-and-hardening-your-ssh-setup\"><\/a><\/p>\n<h2>Prevention and Hardening Your SSH Setup<\/h2>\n<p>A fixed outage is only half a job. If SSH broke once because nobody noticed a drifted config, stale firewall rule, or undocumented custom port, it can break again at the worst possible moment.<\/p>\n<p><figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog-origin.donely.ai\/wp-content\/uploads\/2026\/07\/ssh-port-22-connection-refused-ssh-security.jpg\" alt=\"A professional infographic titled SSH Security Best Practices outlining five essential steps for securing server connections.\" \/><\/figure><\/p>\n<p><a id=\"fix-the-outage-then-reduce-the-blast-radius\"><\/a><\/p>\n<h3>Fix the outage, then reduce the blast radius<\/h3>\n<p>The short-term lesson is simple: don&#039;t leave SSH in a fragile state. Verified benchmark data says <strong>restarting <code>sshd<\/code> resolves approximately 40% of transient \u201cnot listening\u201d failures in cloud VMs<\/strong>, and <strong>30% of SSH connectivity failures come from third-party software modifying <code>sshd_config<\/code> and causing OpenSSL version mismatches that stop the service from starting<\/strong> in this benchmark summary.<\/p>\n<p>That tells you two things. First, transient failures are real, so monitoring the service matters. Second, config drift is a bigger threat than many teams think.<\/p>\n<p><a id=\"build-a-safer-default-ssh-posture\"><\/a><\/p>\n<h3>Build a safer default SSH posture<\/h3>\n<p>Use a hardened baseline:<\/p>\n<ul>\n<li><strong>Prefer keys over passwords:<\/strong> Public key auth removes a lot of brute-force exposure and cuts down on weak credential problems.<\/li>\n<li><strong>Disable direct root login:<\/strong> Use a named user and escalate with <code>sudo<\/code> when needed.<\/li>\n<li><strong>Restrict who can connect:<\/strong> <code>AllowUsers<\/code>, <code>AllowGroups<\/code>, and source IP restrictions help when applied carefully and tested.<\/li>\n<li><strong>Use fail2ban or equivalent tooling:<\/strong> Automatic bans reduce noise and repeated attack attempts.<\/li>\n<li><strong>Treat custom ports as a minor noise filter, not a primary defense:<\/strong> They reduce random scans but don&#039;t replace real hardening.<\/li>\n<\/ul>\n<p>If you&#039;re deciding whether to keep SSH on 22 or move it, this overview of <a href=\"https:\/\/arphost.com\/ssh-default-port\/\">SSH port risks<\/a> is a useful operational perspective. The answer is usually layered security, not one magic setting.<\/p>\n<p>For organizations running automation, customer workflows, or sensitive integrations, SSH policy should align with broader platform controls like access boundaries, auditing, and incident response. That&#039;s the same mindset reflected in a formal <a href=\"https:\/\/donely.ai\/security-policy\">security policy<\/a>.<\/p>\n<p>The admins who avoid repeat outages do one extra thing after recovery: they write down the actual port, the actual firewall path, the expected service name, and the rollback steps. Documentation sounds boring until a midnight reboot proves it isn&#039;t.<\/p>\n<hr>\n<p>If you&#039;re tired of babysitting SSH on headless AI infrastructure, <a href=\"https:\/\/donely.ai\">Donely<\/a> gives you a simpler path. You can host, deploy, and manage AI employees from one dashboard, with isolated instances, centralized logs, RBAC, and security controls that reduce the DevOps drift behind outages like this. It&#039;s a practical option when you want the agents, not the server firefighting.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You&#039;re probably here because a deploy worked yesterday, your VM is still up, and now your terminal says ssh: connect to host \u2026 port 22: Connection refused. That message feels simple, but it&#039;s useful. It usually means the server answered your TCP knock and said nothing is listening there, or something on the path rejected [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":722,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[254,256,252,253,255],"class_list":["post-723","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-agents","tag-connection-refused","tag-server-admin","tag-ssh-port-22-connection-refused","tag-ssh-troubleshooting","tag-sshd"],"_links":{"self":[{"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/posts\/723","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/comments?post=723"}],"version-history":[{"count":1,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/posts\/723\/revisions"}],"predecessor-version":[{"id":728,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/posts\/723\/revisions\/728"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/media\/722"}],"wp:attachment":[{"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/media?parent=723"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/categories?post=723"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog-origin.donely.ai\/blog\/wp-json\/wp\/v2\/tags?post=723"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}