Check an SSH connection without running anything
Verify that an SSH host is reachable, key auth works, and the server is responsive — without actually opening a shell or running a payload command. The canonical "is the box up + is my key configured" health-check used in deploy scripts and CI preflight.
How to check an ssh connection without running anything in each shell
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exitCanonical CI/deploy preflight. `-T` disables tty allocation; `-q` quiets the banner; `BatchMode=yes` disables ALL prompts (password + passphrase + hostkey-accept) — passes only if key auth succeeds; `ConnectTimeout=5` bounds the TCP handshake; the literal command `exit` does nothing useful but is required because OpenSSH won't connect "with no command" silently (it would open a shell, which we want to skip). Exit code: `0` = SSH connected + authenticated + `exit` ran fine; `255` = ssh itself failed (DNS, TCP refused, auth failed, timeout). Use `0 = healthy, anything else = unhealthy` in deploy preflight: `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 prod-bastion exit || { echo "bastion not reachable"; exit 1; }`. CAVEAT: this verifies KEY AUTH, not the remote service — if you need port 22 reachability before key setup, use the netcat form (see notes).
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exitssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exitNo fish-side wrinkle — ssh flags parse identically. To wrap as a fish function for repeat use: `function ssh-check; ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 $argv exit; end` in `~/.config/fish/functions/ssh-check.fish`. Then `ssh-check prod-bastion; and echo OK; or echo DOWN`. fish handles `$argv` cleanly when callers pass multiple args like `ssh-check user@host -p 2222`.
Test-NetConnection -ComputerName host -Port 22 -InformationLevel Quietpwsh-native form. `Test-NetConnection -Port 22` does a TCP probe to port 22 — checks REACHABILITY only, not SSH auth. Returns `$true`/`$false` with `-InformationLevel Quiet`. For full SSH-auth check, pwsh ships OpenSSH so the ssh-flag form works identically: `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit; $LASTEXITCODE`. Combine both for two-phase preflight: `if (Test-NetConnection host -Port 22 -InformationLevel Quiet) { ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit; if ($LASTEXITCODE -eq 0) { Write-Host "OK" } else { Write-Host "Port 22 open but SSH auth failed" } } else { Write-Host "Port 22 unreachable" }`. The legacy WinRM-side equivalent: `Test-WSMan -ComputerName host` (probes the Windows-Remote-Management endpoint, not SSH).
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exitSame OpenSSH binary. Exit code in cmd: `%ERRORLEVEL%`. CMD does not have a `Test-NetConnection` analogue; the closest is `powershell -Command "Test-NetConnection -ComputerName host -Port 22 -InformationLevel Quiet"`. Legacy alternative without ssh: `telnet host 22` — opens TCP and prints the SSH banner (e.g. `SSH-2.0-OpenSSH_8.9p1`). Exit telnet with Ctrl+]+`quit`. CAVEAT: `telnet` is NOT installed by default on modern Windows; install via `dism /online /Enable-Feature /FeatureName:TelnetClient`. The PuTTY-suite alternative: `plink -batch -ssh user@host exit` — `-batch` disables prompts, `exit` is the no-op remote command.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- **Exit-code contract: rc 0 = full success, rc 255 = ssh failed.** The `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit` invocation has a clean exit-code contract: `0` means the TCP connection succeeded, key auth succeeded, AND the remote `exit` command ran cleanly. `255` means ssh ITSELF failed — could be DNS resolution, TCP refused, ConnectTimeout, auth refused, hostkey verification failed. Distinguish these in CI by capturing stderr: `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit 2>err.log; rc=$?; case $rc in 0) echo OK ;; 255) cat err.log; echo "ssh layer failed" ;; *) echo "remote exit $rc" ;; esac`. Common stderr messages: `Connection refused` (no sshd listening), `Connection timed out` (firewall dropping), `Permission denied (publickey)` (auth failed), `Host key verification failed` (server hostkey changed).
- **`nc -z` for port-reachability-only check (no SSH protocol involvement).** When you don't care about SSH auth — just "is port 22 open" — `nc -z -w 5 host 22; echo $?` (`-z` = scan, no data sent; `-w 5` = 5s timeout). rc 0 = open, non-zero = closed/filtered/timed out. Faster than ssh because no protocol negotiation. CAVEAT: `nc` (netcat) has multiple incompatible implementations — `nc.openbsd` (Debian default), `nc.traditional` (older), `ncat` (Nmap project). `-z` works on most; `nmap -p 22 host` is the most portable scan ("STATE: open / closed / filtered"). Use `nc -z` for "is it up before I try ssh", use the full ssh-T check for "is my entire deploy path healthy". `bash` has a built-in TCP probe that needs no extra binary: `timeout 5 bash -c "echo > /dev/tcp/host/22" 2>/dev/null && echo open` — opens TCP via the `/dev/tcp/<host>/<port>` magic path. Useful in minimal containers without nc installed.
- **Hostkey acceptance and `BatchMode=yes` interaction.** First-time ssh to a new host prints `The authenticity of host 'host' can't be established. ECDSA key fingerprint is SHA256:...`. With `BatchMode=yes` this prompt becomes a HARD FAILURE — the script fails with rc 255 + stderr `Host key verification failed`. For CI / preflight where the hostkey isn't pre-populated, options: (1) Pre-populate `~/.ssh/known_hosts` via `ssh-keyscan host >> ~/.ssh/known_hosts` (fetches the server's public hostkey out-of-band; vulnerable to MITM if the network you scan from is compromised — pin the key from a known-good source). (2) Use `-o StrictHostKeyChecking=accept-new` (OpenSSH 7.6+) — auto-accept first-time-seen keys, but error if a key has CHANGED (TOFU model). (3) `-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null` — skip ALL checks (insecure; only for throwaway CI to known-isolated networks). The pinned approach is safest for prod.
- **Distinguishing CONNECTIVITY failures: DNS vs TCP vs SSH-protocol.** Layered diagnosis: (1) DNS: `getent hosts host` (Linux) / `Resolve-DnsName host` (pwsh) / `nslookup host` (universal). Empty result = DNS broken. (2) Ping: `ping -c 1 -W 2 host` (Linux) / `Test-Connection -Count 1 host -TimeoutSeconds 2` (pwsh). Non-zero rc = ICMP blocked or host down — but many cloud envs drop ICMP, so `ping` failing doesn't prove SSH is down. (3) TCP-22: `nc -z -w 5 host 22` or `bash -c "echo > /dev/tcp/host/22"`. Open = sshd is listening. (4) SSH protocol: `ssh -v user@host exit 2>&1 | head -20` (verbose first 20 lines show the SSH handshake — key exchange, ciphers, hostkey verification). The first error line in `-v` output names the failing layer precisely. (5) Auth specifically: `ssh -v user@host 2>&1 | grep -E "Offering|Authentication" ` — shows which key files were tried and what the server replied.
- **ServerAliveInterval / TCPKeepAlive for "is the EXISTING connection still alive".** Different question from "can I newly connect": "is my open ssh session still healthy". OpenSSH options `ServerAliveInterval 30` + `ServerAliveCountMax 3` (in `~/.ssh/config`) send an encrypted-channel keepalive every 30s; after 3 missed responses (90s total), ssh disconnects with `Timeout, server not responding`. `TCPKeepAlive yes` (default) is a separate, OS-level TCP keepalive — coarser-grained (often 2-hour default before first probe). For tunneled connections behind aggressive NAT timeouts (mobile / hotel WiFi often drop idle TCP at 5–10 min), set `ServerAliveInterval 30` aggressively. For "verify health of an active session from another shell": `ssh -O check -S ~/.ssh/cm-%h-%p-%r user@host` (requires ControlMaster). rc 0 = master alive; non-zero = dead.
Related tasks
- Copy files over SSH— Move one or more files between your local machine and a remote host over an SSH-encrypted channel — the standard `scp` / `rsync` / `pscp` operation. The canonical "deploy this build artifact" or "pull these logs back for inspection" gesture.
- Run a remote command non-interactively— Execute a single command on a remote host over SSH without opening an interactive shell — capture its stdout/stderr/exit-code on the local side. The canonical "ssh user@host 'systemctl status nginx'" gesture used in deploy scripts, CI, and ad-hoc remote inspection.
- Set up SSH key authentication— Generate a local SSH key pair and install the public half on a remote host so subsequent SSH/SCP/rsync sessions authenticate without typing a password. The standard onboarding step for any developer touching remote machines.
- Forward a local port over SSH— Tunnel TCP traffic from a port on your local machine to a service reachable from the remote SSH host — bypass NAT, expose an internal database to your laptop, route browser traffic through a jump host, etc. The canonical "ssh -L 5432:localhost:5432 bastion" pattern used to reach private services without VPNing.