An SSH MCP server for AI agents: reverse tunnels and the three ways they break

The short answer

An SSH MCP server lets an assistant such as Claude run commands on a remote machine, and a reverse tunnel opened by that machine means it needs no open port. It breaks in three quiet ways: the tunnel gets started on the wrong machine and loops back, the remote machine regenerates its host key on restart, and OpenSSH's PerSourcePenalties locks out a connector that probes the port. Check the remote identity on every command, keep the host key somewhere persistent, and never probe by connecting.

Why a reverse tunnel?

My MCP connector runs on my own server; the machine Claude operates is a cloud computer I do not fully control. Rather than open a port on it, that machine runs ssh -N -R localhost:2222:localhost:22 to my server. The connector then reaches it at localhost:2222. Nothing on the remote machine listens on the internet, and on my side the key it uses is restricted so it can hold that one tunnel and nothing else:

restrict,port-forwarding,permitlisten="localhost:2222",command="/bin/false" ssh-ed25519 AAAA... remote

Every tool becomes one command over SSH with ControlMaster multiplexing, so after the first call there is no new handshake.

Failure 1: the tunnel that loops back

The tunnel script was once run on my server instead of the remote machine. Port 2222 then forwarded my server to itself. Logins worked, commands ran, and every check looked healthy, on the wrong computer. Only comparing /etc/machine-id at both ends showed it.

The fix is a preamble on every command that reads the remote machine's id and refuses to run if it is unreadable or equals the local one. Checking once per connection is not enough; a reconnect can slip past it. The command's real exit status comes back with an end marker that carries a per-call random token, so output from the command itself can never pass for the connector's signals.

Failure 2: host keys that change on restart

The connector pins the remote host key, as it should. Then the remote machine restarted and came back with a new key. It turned out the platform resets /etc on every restart and keeps only the home directory, so the SSH server generated fresh keys each time. With pinning, every restart would have looked like an impostor.

The fix is to keep the host key in the persistent home directory and start sshd -h ~/.ssh/host_key from a small keeper script, which also reopens the tunnel in a loop. A restart now changes nothing the connector can see.

Failure 3: locked out by your own health check

OpenSSH 9.8 and later include PerSourcePenalties, which penalise a source address that connects repeatedly without completing a login. A health check that opens a TCP connection to the tunnel port and closes it is exactly that. Through a reverse tunnel, every connection reaches the remote SSH server from the same local address, so a few probes would lock the connector out of the machine entirely. The implementer of my SSH layer hit this in testing; it shows up as connections being refused for no obvious reason.

The fix: check that the tunnel port is listening by reading the local socket table instead of connecting. On Linux that is /proc/net/tcp, state 0A.

Two settings that make tunnels self-healing

  • On the tunnel client: ServerAliveInterval 30, ExitOnForwardFailure yes and a loop that retries after ten seconds.
  • On the server: ClientAliveInterval 30 with ClientAliveCountMax 3, so a dead tunnel frees its port in about 90 seconds and the client can reclaim it.

With those in place my connector survives restarts and network drops without anyone touching it, and when something is down its status tool says which part and the exact command that fixes it.

Questions people ask

What is an SSH MCP server?

An MCP server whose tools run commands and read or write files on a remote machine over SSH, so an assistant such as Claude can operate that machine through named tools with confirmation and logging.

Why use a reverse SSH tunnel for an AI agent?

The remote machine opens the connection outward, so it needs no open inbound port. The connector reaches it through a local port on the server, and the tunnel key can be restricted to that single forward.

Why does SSH suddenly refuse connections from localhost?

OpenSSH 9.8+ PerSourcePenalties may be penalising the address. Through a reverse tunnel every connection arrives from the same local address, so repeated connections that never log in, such as port probes, trigger it.

How do I stop host key warnings after a machine restarts?

If the platform resets /etc, store the SSH host key in a persistent directory and start sshd with -h pointing at it, then pin that key on the client.

About the author. Muhammad Tayyab Ilyas is an Applied AI & Solutions Engineer in Barcelona who builds and operates MCP servers, multi-agent systems and the infrastructure under them. His SSH-based connector lets Claude supervise a team of Grok Bots.