Running into a "Connection refused" error on port 22 is super common, and more often than not, it means your ISP has quietly tucked you behind Carrier-Grade NAT (CGNAT). When that happens, your regular router port forwarding essentially leads to nowhere. Getting past it comes down to figuring out what kind of IP your ISP is actually handing you so you can pick between a standard dynamic DNS setup or a mesh VPN like Tailscale.

  • Default Port: 22 (Swap this out right away for something custom like 2222)
  • Windows Requirement: Windows 10 (build 1709) or newer
  • Linux Requirement: The standard openssh-server package
  • Key Algorithm: Ed25519 (Faster, cleaner, and way more solid than aging RSA keys)
  • Remote Access Methods: Standard Port Forwarding (Static IP/DDNS) or a Mesh VPN (Tailscale)

SSH Server Decision Tree: Port Forwarding vs. Tailscale

If your provider actually hands you a real public IP address, traditional port forwarding paired with Dynamic DNS is straightforward and keeps you in full control of your traffic. But if you log into your router's admin dashboard and see a WAN IP kicking off with 10.x.x.x or 100.x.x.x, you're definitely behind CGNAT. In that scenario, Tailscale saves you a ton of headaches by tunneling straight through without you needing to touch any router rules.

Step 1: Installing OpenSSH on Your Local Machine

Ubuntu/Debian Setup (Linux)

Fire up your terminal and run sudo apt update && sudo apt install openssh-server. The daemon spins up on its own the second the package finishes installing. You can verify it's running properly by running sudo systemctl status ssh. If things are working as expected, you'll see a clean green active (running) status right in the output.

Windows Native OpenSSH Setup (And the administrators_authorized_keys Trap)

Both Windows 10 and 11 come with OpenSSH server built right into the OS, so you don't need third-party tools. Just pop open PowerShell as Administrator and run Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0. Once that finishes, start it with Start-Service sshd and make sure it boots every time your system starts by running Set-Service -Name sshd -StartupType 'Automatic'.

Now, keep an eye out for this massive gotcha on Windows: if your user account has administrative privileges, OpenSSH will flat-out ignore your usual ~/.ssh/authorized_keys file. You have to drop your public key into C:\ProgramData\ssh\administrators_authorized_keys instead. On top of that, NTFS permissions on this file are notoriously strict - it will fail unless only SYSTEM and Administrators have access, so make sure to strip out permissions for everyone else under the file's Security properties.

If you're running Windows virtual machines alongside your main host using Hyper-V on Windows 11, keep in mind that every single VM manages its own SSH setup and key files. That means the same CGNAT quirks and the administrators_authorized_keys permission headache apply identically inside each virtual guest.

Step 2: Hardening Security and Key-Based Authentication

Generating and Copying SSH Keys

Skip passwords entirely for remote logins; keys are just far safer and way more convenient. On your client machine, run ssh-keygen -t ed25519 -C "your_email@example.com". Picking Ed25519 gives you a lightweight, highly secure key that stands up far better to modern brute-force guessing than legacy options. Once created, ship the public key over to your Linux box using ssh-copy-id username@target_ip.

Disabling Root Login and Password Authentication

Open up /etc/ssh/sshd_config on Linux (or head to C:\ProgramData\ssh\sshd_config if you're on Windows) in your favorite editor. Look for #PasswordAuthentication yes, uncomment it, and switch it over to PasswordAuthentication no. Do the exact same thing for PermitRootLogin no. Make sure to restart the SSH service right after saving so those changes actually take effect.

Step 3: Beating Dynamic IPs and Carrier-Grade NAT

Setting up DDNS (DuckDNS / ddclient)

Most residential internet connections shuffle your public IP whenever your gateway reboots. To handle this, grab a free subdomain from DuckDNS and throw ddclient on your Linux machine. This little background daemon checks in with DuckDNS every few minutes to make sure your custom domain always points to whatever IP address your home network is currently using.

Port Forwarding Basics on a Home Router

Hop into your router's web interface - most commonly sitting at 192.168.1.1 or 192.168.0.1. Find the Port Forwarding or Virtual Server settings tab. Set up a forwarding rule that takes external port 2222 and routes it directly to internal port 22 on your machine's local IP. Leaving port 22 directly exposed to the open internet just floods your auth logs with non-stop script-kiddie scanners.

The Tailscale Alternative (Zero Router Config)

If port forwarding sounds like a hassle or CGNAT is blocking you, grab Tailscale on both your laptop and your target home box. It builds a private, encrypted mesh network and gives your machine a dedicated 100.x.x.x address. From there, you just run ssh username@100.x.x.x from anywhere you have an internet connection. It connects seamlessly, and your router firewall stays completely shut tight.

Step 4: Adding Fail2Ban for Brute Force Protection

Any time you open a port to the public web, random bots will inevitably come knocking. You can keep them at bay on Linux by installing Fail2Ban via sudo apt install fail2ban. It constantly watches your auth logs for bad login patterns and automatically drops offender IPs right at the firewall. It defaults to a 10-minute timeout, but it's worth bumping that up to 24 hours inside your /etc/fail2ban/jail.local configuration.

SSH Troubleshooting: Decoding Connection Errors

Connection refused vs Connection timed out vs Permission denied

Connection refused: Your packets made it to the box, but nothing is accepting connections on that port - either the SSH daemon isn't running or a local software firewall (like UFW or Windows Defender) is actively rejecting it. If you know the daemon is up, take a second to check what process is holding port 22 by running ss -tlnp | grep :22.

Connection timed out: Your connection attempt is vanishing into thin air before reaching the machine. Nine times out of ten, this means your router port forwarding rule points to the wrong internal IP, or your ISP is dropping incoming traffic right at the edge of their network.

Permission denied (publickey): You hit the SSH server just fine, but it doesn't trust your key. If you're on Windows, check that administrators_authorized_keys file and its strict NTFS permissions again, or make sure your SSH client is actually offering the specific private key that matches the user on the target box.

Bonus: Using ~/.ssh/config for Quick Access Aliases

There's no reason to type out full IPs, usernames, and custom ports every single time you want to log in. You can drop a plain text file at ~/.ssh/config on your machine and organize your hosts cleanly. Just create an entry setting your HostName, User, and Port underneath a friendly tag like Host home-pc. Next time you need in, all you have to run is ssh home-pc.