SSH reverse tunnels let you connect to a machine that’s behind a NAT or firewall, which you normally couldn’t reach directly.

Let’s say you have two machines:

  • local-machine: Your laptop, sitting on your home network (behind NAT).
  • remote-server: A publicly accessible server (e.g., a VPS) with a static IP address.

You want to SSH from remote-server back to local-machine. This is the reverse of the usual flow.

Here’s how it works:

From local-machine, you initiate an SSH connection to remote-server. As part of this connection, you tell remote-server to listen on one of its ports. When something connects to that port on remote-server, the traffic is forwarded back through the established SSH connection to local-machine and then directed to a specified port on local-machine.

The Command (from local-machine):

ssh -R 8080:localhost:22 user@remote-server-ip

Let’s break this down:

  • ssh: The standard SSH client.
  • -R 8080:localhost:22: This is the core of the reverse tunnel.
    • 8080: The port on remote-server that will be opened and listened upon. Anyone connecting to remote-server:8080 will have their traffic tunneled.
    • localhost: This refers to the local-machine from the perspective of the SSH client process running on local-machine.
    • 22: The port on localhost (which is local-machine) that the tunneled traffic will be forwarded to. This is typically the SSH port on your local-machine.
  • user@remote-server-ip: Your login credentials for the remote-server.

Once this command is running on local-machine, you can do the following:

  1. On remote-server, SSH to yourself (to localhost) on the port specified in the -R flag.

    ssh -p 8080 user@localhost
    
    • ssh: SSH client.
    • -p 8080: Connect to port 8080 on remote-server.
    • user@localhost: Connect as user to localhost. Since you’re already on remote-server, localhost refers to remote-server itself.

    This connection will be forwarded back through the initial tunnel to local-machine’s port 22. You’ll be prompted for the password for user on local-machine.

  2. Alternatively, from any other machine that can reach remote-server, you can SSH to remote-server on port 8080.

    ssh -p 8080 user@remote-server-ip
    

    This will also connect you to your local-machine.

Making it Robust:

The basic ssh -R command is fine for a quick test, but it’s fragile. If the SSH connection drops, the tunnel breaks. For more reliable access, you’ll want to use autossh.

autossh is a utility that monitors an SSH connection and restarts it if it fails.

The autossh Command (from local-machine):

autossh -M 0 -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" -R 8080:localhost:22 user@remote-server-ip
  • autossh: The command.
  • -M 0: This tells autossh not to use a monitoring port. Instead, it relies on SSH’s ServerAlive mechanism.
  • -o "ServerAliveInterval 60": Tells the SSH client to send a "keep-alive" message every 60 seconds.
  • -o "ServerAliveCountMax 3": If 3 keep-alive messages go unanswered, SSH will consider the connection dead.
  • -R 8080:localhost:22 user@remote-server-ip: These are the same reverse tunnel options as before.

You’ll typically want to run autossh in the background and use SSH keys for passwordless authentication.

Example autossh in the background with key-based auth:

First, ensure you have SSH keys set up: ssh-keygen on local-machine, then ssh-copy-id user@remote-server-ip.

autossh -M 0 -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" -N -f -R 8080:localhost:22 user@remote-server-ip
  • -N: Do not execute a remote command. This is useful for just forwarding ports.
  • -f: Go into background after authentication.

Configuration on the remote-server:

For the reverse tunnel to work correctly, especially if you want other machines to connect to remote-server:8080, you might need to adjust GatewayPorts in the sshd_config file on remote-server.

By default, GatewayPorts is often set to no or clientspecified. This means that the port opened by -R (e.g., 8080) will only be bound to localhost on remote-server. So, only processes running on remote-server can connect to it.

To allow external connections to remote-server:8080, you need to set GatewayPorts yes in /etc/ssh/sshd_config on remote-server and then restart the SSH service: sudo systemctl restart sshd.

With GatewayPorts yes, the -R 8080:localhost:22 command will bind port 8080 to 0.0.0.0 on remote-server, making it accessible from anywhere.

Why GatewayPorts clientspecified is often preferred:

If you set GatewayPorts yes, all reverse tunnels will bind to 0.0.0.0. This might be a security concern if multiple users are on remote-server and one of them sets up a reverse tunnel.

A more granular approach is GatewayPorts clientspecified. With this setting, the behavior of GatewayPorts depends on how the reverse tunnel is specified. If you use ssh -R *:8080:localhost:22 ... (note the asterisk), it will bind to 0.0.0.0. If you use ssh -R 8080:localhost:22 ... (without the asterisk), it will bind only to localhost on the server.

So, on remote-server, setting GatewayPorts clientspecified in sshd_config (and restarting sshd) allows you to control whether a specific reverse tunnel is globally accessible or only accessible from the remote-server itself. For general access from anywhere, you’d use ssh -R *:8080:localhost:22 user@remote-server-ip.

The most common pitfall after setting up a working reverse tunnel is forgetting that the remote-server’s SSH daemon (sshd) must be configured to allow remote port forwarding to bind to non-localhost addresses if you want to access it from machines other than the remote-server itself. You’ll find yourself SSHing to remote-server:8080 and getting "connection refused" even though the tunnel is active on local-machine.

Want structured learning?

Take the full Ssh course →