22
The port sshd listens on — and behind it `scp`, `rsync`, `git` over SSH, tunnels and SFTP, which is why one refused connection here breaks five different things at once.
Run the line for your platform. Each one names the process holding the port and the address it bound to, which is usually the whole answer.
sudo lsof -nP -iTCP:22 -sTCP:LISTENOne row per listening socket: COMMAND, PID, USER, then NAME, which holds the bind address. `*:22` is every interface, while `127.0.0.1:22` is loopback only — a legitimate configuration for a tunnel endpoint, and a complete explanation for a remote client that only ever times out. `-n` stops lsof resolving addresses to host names and `-P` stops it printing `ssh` in place of `22`. `-sTCP:LISTEN` hides established sessions; drop it to list who is currently connected. No output at all, with lsof exiting 1, means nothing holds the port, and on macOS that is the normal state until Remote Login is switched on in System Settings. Without `sudo` you see only your own processes, so a root-owned daemon looks like it is missing.
sudo ss -tlnp 'sport = :22'`-t` selects TCP, `-l` only listening sockets, `-n` keeps the port numeric and `-p` adds the owning process; `sport = :22` is ss's filter predicate for the source port. Read Local Address:Port first — `0.0.0.0:22` is every IPv4 interface, `127.0.0.1:22` is loopback only, and `[::]:22` is the IPv6 wildcard, which on most Linux hosts accepts IPv4 too. The owner appears as `users:(("sshd",pid=812,fd=3))`, or as `users:(("systemd",pid=1,fd=53))` on a host where SSH is socket-activated, which is expected rather than suspicious. Without `ss` installed, the net-tools spelling is `sudo netstat -tlnp | grep ':22 '`, where `-p` prints a slash-separated pid/program pair; keep the trailing space or the pattern also matches `:2222`. Both need superuser privileges to show processes you do not own, and without them the process column is simply blank.
netstat -ano | findstr ":22"`-a` shows connections and listening ports, `-n` keeps addresses and ports numeric, and `-o` puts the owning process ID in the last column. The row you want has State LISTENING; anything else is a client session. Resolve the PID with `tasklist /FI "PID eq 812"`, whose filter accepts eq, ne, gt, lt, ge and le — on Windows the listener is normally `sshd.exe` from the optional OpenSSH Server feature, which is not installed by default. Be careful with the match: `findstr` compares substrings, so `":22"` also hits `:2222`, `:22000` and ephemeral ports containing 22. Read the whole Local Address column before acting.
Get-NetTCPConnection -LocalPort 22 | Select-Object LocalAddress,LocalPort,State,OwningProcessMatches the port numerically instead of by substring, so it never picks up `:2222` the way `findstr` does. `State` of `Listen` with `LocalAddress` `0.0.0.0` means every interface; `127.0.0.1` means the machine itself only. Pipe `OwningProcess` into `Get-Process -Id` to name the executable. An empty result while a bind keeps failing points at a reservation rather than an occupant — `netsh interface ipv4 show excludedportrange protocol=tcp` lists the ranges Hyper-V, WSL2 and Docker Desktop claim at boot, and a port inside one of them cannot be bound by anything and has no PID to blame.
`nmap -Pn -p 22 host.example.com` reports what the port looks like from outside the machine, and `-Pn` treats the target as online so a host that drops pings still gets scanned. Nmap's definitions are precise: `open` means an application is actively accepting connections there; `closed` means the port is reachable and answered but nothing is listening on it, which is the shape of a stopped sshd; `filtered` means packet filtering kept the probe from reaching the port, so nmap cannot tell open from closed — and that is the same firewall that made your client hang instead of failing fast. `--reason` prints what actually came back, `syn-ack` for open and `conn-refused` for closed. `-sV` reads the version banner, which for SSH is the plaintext identification string the protocol sends before any encryption, something like `SSH-2.0-OpenSSH_9.6`. That string is also exactly what internet-wide scanners collect, so treat its contents as public. Scan only hosts you are responsible for.
| What you see | What it means |
|---|---|
ssh: connect to host host.example.com port 22: Connection refused | The host is up and answered — its kernel sent a reset because no process holds port 22. sshd is stopped or failed to start, it is bound to an address that does not include the one you dialled, or it now listens on a different port after a config change. This failure is instant, and that speed is the useful signal: the network path works and only the service is missing. Run the probe for the platform on a console or another route into that machine. |
ssh: connect to host host.example.com port 22: Connection timed out | The opposite diagnosis, and the reason the two are worth telling apart. Nothing came back at all: a firewall, a cloud security group or a network ACL is dropping the SYN silently, or you are dialling an address that no longer belongs to that machine. macOS phrases the same thing as `Operation timed out`. The tens of seconds you waited are the giveaway — a refusal returns immediately, a drop makes you sit through the connect timeout. Confirm with `nmap --reason`: `filtered` is the drop. |
Permission denied (publickey). | TCP, the port and the daemon are all fine — you reached authentication and it failed, and the message names the only method the server was willing to try. Work in order. Run `ssh -v` and read the `Offering public key` lines to see which key the client actually sent: an agent holding many keys can exhaust the server's attempt limit before it reaches the right one, so pin it with `-i` and `IdentitiesOnly=yes`. Then check the server side, where the key must sit in that account's `~/.ssh/authorized_keys`. OpenSSH's `StrictModes` defaults to yes, so a group- or world-writable home directory, `.ssh` or `authorized_keys` makes sshd ignore the file without telling the client anything — `~/.ssh` at 700 and `authorized_keys` at 600, owned by that user. The server log states the real reason in one line; the client never will. |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! ... Host key verification failed. | The key the server presented is not the one recorded for that name in `known_hosts`, and this is the one error where the reflex answer is the wrong one. Do not start with `ssh-keygen -R`: that deletes the only record you have of what the host used to be. There are innocent causes — the machine was rebuilt, a cloud address was released and reassigned to somebody else's instance, the name now resolves to a different backend, or you are reaching a different host through a forwarded port — and one that is not, which is somebody sitting between you and the server. Establish which first: compare the fingerprint ssh printed against one taken on the console with `ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key`, or against the fingerprint in the instance's boot log. `ssh-keygen -F host` shows the stored entry and where it lives. Only once the new fingerprint is confirmed does `ssh-keygen -R host` become the right command. |
error: Bind to port 22 on 0.0.0.0 failed: Address already in use. | sshd cannot start because something already holds the socket. Usually the old sshd is still running — a restart that raced its own shutdown, or a hand-built install competing with the packaged service — and sometimes a socket unit holds 22 while you also start the service unit. Name the owner with the probe above rather than guessing, and never stop the running sshd from inside a session that depends on it without a console or a second way in. |
ssh_exchange_identification: Connection closed by remote host | The TCP connection succeeded and sshd hung up before the protocol banner, so the port, the route and the firewall are all fine. The usual causes are a ban on your source address from fail2ban or an equivalent, `MaxStartups` shedding connections while the machine is under load or out of file descriptors, and an access rule such as `AllowUsers`, `DenyUsers` or a TCP wrappers entry. This is a server-side decision about you, not a network fault, and the server's log says which. |
IANA assigns 22 to `ssh`, the Secure Shell protocol, over TCP, UDP and SCTP; in practice only TCP is used. A single listener on this port carries much more than an interactive shell: `scp`, `rsync` run over SSH, `git clone git@host:repo`, `ssh -L` and `-D` tunnels, `ProxyJump` bastion hops, and the SFTP subsystem — which is all that SFTP is, and which has nothing whatsoever to do with FTP on port 21. OpenSSH's documented default is `Port 22` in `sshd_config`, and `ListenAddress` decides which interfaces it binds; both directives may appear more than once, so a server can legitimately answer on 22 and on a second port at the same time. Before you go hunting for a process, know that several distributions now ship a systemd socket unit for SSH: where `ssh.socket` is the enabled unit, the process holding port 22 is `systemd` and an `sshd` only exists once a connection arrives. That is normal, and it is not evidence that something else took the port. Moving SSH off 22 is also more than one line. Change `Port` in `sshd_config`, open the new port in the host firewall, and on a SELinux system register it as well with `semanage port -a -t ssh_port_t -p tcp 2222` — skip that last step and sshd starts, logs a bind failure, and you are locked out of a machine that looks perfectly healthy from every other angle. Always test a new port from a second session while the first one is still open.
Port 22 is one of the few management ports it is defensible to expose, because the transport is encrypted and the client checks the server's host key before it sends anything secret. That is not the same as safe. A listener on 22 is discovered within hours and then receives a constant, dull stream of credential guesses against `root`, `admin`, `ubuntu`, `git` and a few hundred other names. The controls that change the outcome are: keys only, with `PasswordAuthentication no` — OpenSSH's own documented default for that option is `yes` and distributions override it in both directions, so read the running configuration instead of assuming; `PermitRootLogin` at `prohibit-password`, which is the documented default, or `no`; and a rate limiter such as fail2ban, which bans a source address after a handful of failures and mostly buys you readable logs. Moving the daemon to a high port cuts the noise dramatically and cuts the risk barely at all, because a targeted scan finds it in seconds. The reductions that count are restricting source addresses at the firewall, putting the whole fleet behind one bastion reached with `ProxyJump` so only that host answers on 22, or requiring a VPN first. Whichever you pick, verify it from a second session that is already logged in — every lockout starts with a change tested only by reconnecting.
All of these run in your browser — nothing is uploaded.
Ports people usually end up checking in the same session as 22.
Almost always permissions. lsof and ss list the socket but hide the owning process when it belongs to another user, so a server started by a system account, by a container runtime or by launchd shows up as an anonymous listener until you re-run the command with sudo. On Windows the opposite happens: netstat shows nothing at all, yet a bind still fails, which means the port sits inside a range that Hyper-V, WSL2 or Docker Desktop reserved at boot.
Look at what it is first. An orphaned dev server is safe to kill; a database with a write in flight is not, and neither is a system service that something else depends on. If the owner is a container, stop the container instead — killing the process the host sees only makes the runtime restart it. When the process is something you need, moving your own service to another port is the faster fix.
The listener bound to 127.0.0.1 rather than 0.0.0.0. The probes on every page print the bound address next to the process for exactly this reason: a loopback bind is reachable from the machine itself and from nowhere else, no matter how the firewall is configured. Many frameworks made that the default deliberately, so it is usually a flag rather than a bug. If the address is already 0.0.0.0 and it still fails, the next suspect is a host firewall or a cloud security group.
Barely. Internet-wide scanners sweep the whole port range continuously, so an unusual number buys hours, not safety, and it costs you every colleague who now has to remember it. It does cut the noise in your logs from opportunistic scans of the default port, which is a real if modest benefit. Authentication, a firewall that restricts source addresses, and not exposing the service at all are the measures that actually change the outcome.
Still stuck on port 22? browse every port in the reference — or go back to the failure message you actually got, above.