You have something running on your laptop — a dev server, an API, a machine at home behind NAT — and something on the other side of the internet needs to reach it. No public IP, no router access, no time to set up a VPN. This is the job of the SSH reverse tunnel, ssh -R: it makes a port appear on a server you can reach, and quietly pipes everything that hits it back to your machine.
It’s the flag people find backwards, so let’s fix the mental model first, then work through the real cases.
The mental model: -L, mirrored
Every SSH tunnel opens a listening port on one machine and ships whatever arrives to somewhere reachable from the other machine. With local forwarding (-L), the port opens on your machine and traffic exits on the server’s side. A reverse tunnel is the exact mirror image:
With -R, the listening port opens on the remote server, and connections to it come out on your side.
That’s the entire trick. The machine that can’t accept inbound connections makes an outbound SSH connection instead — and rides connections back in over it. If the two still blur together, the full family portrait is in SSH port forwarding, explained.
The syntax, read left to right
ssh -R [bind_address:]remote_port:localhost:local_port user@server
Read it as: on the server, listen on remote_port; forward whatever arrives to localhost:local_port — as resolved from my machine. So:
ssh -R 8080:localhost:3000 [email protected]
opens port 8080 on the VPS, and every connection to it lands on port 3000 of your laptop. The destination doesn’t have to be your laptop itself — -R 8080:192.168.1.50:80 forwards to another machine on your LAN, using your laptop as the exit.
One flag worth knowing: -R 0:localhost:3000 lets the server pick any free port, and prints Allocated port 43022 for remote forward so you know which one you got.
Three things people actually build with it
Show a local dev server to a client. Your work-in-progress runs on localhost:3000. Instead of deploying it somewhere just for a demo:
ssh -N -R 8080:localhost:3000 [email protected]
Anyone who can reach demo.example.com:8080 is now looking at the app on your laptop, live. (-N skips the remote shell — you only want the tunnel. Whether anyone or only the server itself can connect depends on GatewayPorts — next section.)
Reach a home machine behind NAT. Your home box can’t accept connections, but it can make them. Have it hold a tunnel out to a small VPS:
# on the home machine
ssh -N -R 2222:localhost:22 [email protected]
Now the VPS’s port 2222 is the home machine’s SSH port, and from anywhere:
ssh -J vps.example.com -p 2222 you@localhost
jumps through the VPS straight into the box behind NAT. This is the classic self-hosted answer to “how do I SSH into my home server.”
Deliver webhooks to your laptop. A payment provider or git host needs to POST to your in-development handler. Tunnel their reachable endpoint back to you:
ssh -N -R 9000:localhost:9000 [email protected]
Point the webhook at the VPS. If you keep the tunnel bound to the server’s loopback (the default), put nginx or any reverse proxy on the VPS in front of 127.0.0.1:9000 — you get TLS termination for free and never expose the raw tunnel port.
GatewayPorts: why nobody else can reach your tunnel
The number-one surprise: the tunnel works when you test it from the server (curl localhost:8080 succeeds) but nobody outside can connect. That’s by design — by default, sshd binds remote-forwarded ports to 127.0.0.1 only, no matter what bind address you asked for.
To open a tunnel to the world, the server’s /etc/ssh/sshd_config needs:
GatewayPorts clientspecified
then restart sshd, and request the bind explicitly: ssh -R 0.0.0.0:8080:localhost:3000 .... (GatewayPorts yes binds every remote forward to all interfaces, which is broader than you usually want.) This is a server-side decision you cannot override from the client — a deliberate safety default, since a reverse tunnel is a hole punched from the inside, and whatever it exposes is now effectively internet-facing. Treat it that way.
Two related server-side switches can bite you: AllowTcpForwarding no disables tunnels entirely, and PermitListen can restrict which ports -R may claim.
Keeping it alive
A reverse tunnel is one TCP connection, and idle TCP connections die. For anything longer than a demo:
ssh -N -R 2222:localhost:22 \
-o ServerAliveInterval=30 \
-o ExitOnForwardFailure=yes \
[email protected]
ServerAliveInterval 30keeps traffic flowing so NAT entries and idle timeouts don’t reap the connection.ExitOnForwardFailure yesmakes ssh die loudly if the remote port can’t be bound, instead of connecting anyway and forwarding nothing.autosshwraps the command and restarts the tunnel whenever it drops — the standard tool for tunnels that should survive reboots and flaky networks, usually run asautossh -M 0 -N -R ...under a systemd unit.
And once a tunnel earns a permanent place, move it into ~/.ssh/config with a RemoteForward line so it travels with the host entry — structure covered in the ssh_config guide:
Host homelink
HostName vps.example.com
User tunnel
RemoteForward 2222 localhost:22
ServerAliveInterval 30
ExitOnForwardFailure yes
-L vs -R vs -D at a glance
| Flag | Port opens on | Traffic exits on | Typical job |
|---|---|---|---|
-L | your machine | server’s side | pull a remote DB/dashboard to localhost |
-R | the server | your side | expose local service; reach a NATed box |
-D | your machine | server’s side, any destination | SOCKS proxy into a network |
When a reverse tunnel is the wrong tool
A reverse tunnel is a superb temporary answer: demos, webhook debugging, rescuing a machine behind NAT. It’s a poor permanent one. If the thing you’re exposing needs to be up for months, serve real users, or survive your laptop sleeping — that’s ingress, and ingress deserves real infrastructure: a reverse proxy on a server, a VPN or overlay network, an actual deployment. The tunnel’s charm is that it’s one command with no standing footprint; the moment you’re writing monitoring for it, you’ve outgrown it.
Troubleshooting the usual failures
Warning: remote port forwarding failed for listen port 8080— the port is taken on the server. Often the culprit is your own previous tunnel: the old sshd process holds the port until the dead connection times out. Keepalives prevent the zombie;-R 0:...sidesteps the collision;ExitOnForwardFailureturns the warning into an error you can’t miss.- Works from the server, refused from outside — the listener is on the server’s loopback. That’s the
GatewayPortsdefault doing its job; either configure it or front the port with a reverse proxy. - Tunnel connects but the service doesn’t answer — check the local half: is anything actually listening on the local port you named?
-R 8080:localhost:3000with nothing on your:3000yields connection refused for every visitor. - The SSH connection itself won’t establish — that’s a different problem with its own checklist: connection refused and timed out, decoded.
When tunnels become furniture
The reverse tunnel you run once, you type. The one that carries your home lab, your demo box and your webhook endpoint is state you rebuild every morning after every laptop sleep — and lose track of by Thursday. Termalin keeps local, remote and dynamic forwards saved as part of the host they belong to: start them with a click, see at a glance which are live, and let them re-establish with the connection instead of being retyped from shell history. The host list — keys, jump hosts, keepalives and tunnels included — syncs across your machines end-to-end encrypted. The command line teaches you what -R means; a client keeps the tunnels you rely on from depending on your memory.
Termalin is a free, cross-platform SSH client with built-in tunnels, SFTP and a key manager — download it, or see how it handles your keys safely.