termal.in

← Blog

SSH port forwarding, explained: local, remote and dynamic tunnels

· Termalin team sshport-forwardingtunnelingtutorial

SSH port forwarding is one of those features people copy off Stack Overflow, get working once, and never quite understand — so the next time they need it, it’s back to guessing which of -L, -R and -D to use. The three flags aren’t hard; they’re just badly named. Here’s the one mental model that makes them obvious, an example for each, and the flags and traps that decide whether the tunnel survives the afternoon.

The one question that picks the flag

Every SSH tunnel opens a listening port on one machine and quietly ships whatever hits it to somewhere reachable from the other machine. So the only question is: which side opens the port you connect to?

  • -L (local) — the port opens on your machine. You connect locally; traffic comes out on the server’s side.
  • -R (remote) — the port opens on the server. Someone connects there; traffic comes out on your side.
  • -D (dynamic) — the port opens on your machine, but it’s a SOCKS proxy: the destination is decided per-connection, not fixed up front.

Everything else is detail. Keep “which side opens the port” in your head and you’ll never reach for the wrong flag again.

Local forwarding (-L) — pull a remote service to localhost

The most common one. A database, an internal dashboard, a metrics UI — something bound to 127.0.0.1 on the server, or on a machine only the server can reach. You want it on your laptop as if it were local.

ssh -L 5432:localhost:5432 [email protected]

Read it left to right: open 5432 on my machine, and forward anything that arrives to localhost:5432 as seen from the server. Now psql -h 127.0.0.1 on your laptop talks to the server’s Postgres — through the encrypted SSH connection, without exposing the database to the network at all.

The middle host is resolved on the server side, which is what makes this powerful:

# reach a database that only the bastion can see
ssh -L 5432:10.0.0.9:5432 [email protected]

Here 10.0.0.9 is a private address your laptop can’t route to — but the bastion can. The tunnel bridges the gap.

Remote forwarding (-R) — expose your machine to the server’s side

The mirror image, and the one people find backwards. The port opens on the server; connections to it come back out on your end. Two classic uses:

# let the server (or its network) reach a dev server running on your laptop
ssh -R 8080:localhost:3000 [email protected]

Now anything hitting localhost:8080 on the server is piped to localhost:3000 on your laptop — handy for showing a teammate on the box your work-in-progress, or letting a remote service call back into something only you’re running.

This is also the trick for reaching a machine stuck behind NAT with no inbound access: have it open a reverse tunnel out to a server you control, and you connect through the server. The machine that can’t accept connections makes the outbound one instead.

One catch: by default the server binds the forwarded port to localhost, so only the server itself can use it. To let other machines on the server’s network reach it, the server’s sshd_config needs GatewayPorts yes (or clientspecified) — a server-side setting, not something you can force from the client.

Dynamic forwarding (-D) — a SOCKS proxy in one flag

-L forwards to one fixed destination. -D opens a SOCKS proxy instead, so the destination is chosen per request:

ssh -D 1080 [email protected]

Point your browser (or curl --socks5 127.0.0.1:1080) at 127.0.0.1:1080 and every request is routed from the server. It’s the quick way to browse an internal network, test what a service looks like from a datacenter’s IP, or reach a handful of internal hosts without a -L for each one.

The flags that make it usable

A bare tunnel command also opens a shell you don’t want and dies the moment you close it. In practice:

ssh -N -L 5432:localhost:5432 [email protected]
  • -N — don’t run a remote command. You only want the tunnel, not a shell.
  • -f — background the process after connecting (pair with -N).
  • -o ExitOnForwardFailure=yes — if the port can’t be bound (already in use, permission denied), fail loudly instead of connecting anyway and silently forwarding nothing.
  • Keepalives — a long-lived tunnel dies to idle timeouts without ServerAliveInterval 30. Add it or the tunnel drops the moment you stop using it.

Make it permanent in ssh_config

Anything you’d pass with -L/-R/-D has a config directive, so a saved host carries its tunnels:

Host db-tunnel
    HostName db.example.com
    User deploy
    LocalForward 5432 localhost:5432
    ServerAliveInterval 30
    RequestTTY no

Now ssh -N db-tunnel brings the database to 127.0.0.1:5432 with no flags to remember. RemoteForward and DynamicForward work the same way. (More on the config file itself in the ~/.ssh/config guide.)

Three traps

  • Bind address matters. -L 5432:... listens on 127.0.0.1 only. To let other devices on your LAN use the tunnel, prefix a bind address: -L 0.0.0.0:5432:... — and understand you’ve just exposed it to your local network.
  • “Address already in use” means the local port is taken (often a previous tunnel that didn’t exit). Pick another local port or kill the old one; ExitOnForwardFailure turns the silent failure into an error you’ll actually notice.
  • The middle host is resolved on the far side. localhost in -L a:localhost:b means the server’s localhost, not yours. Getting this backwards is the single most common port-forwarding mistake.

When juggling tunnels by hand gets old

Forwarding is a great primitive, and every developer should know the three flags cold. The friction shows up later: you keep a scratchpad of ssh -N -L ... commands, you re-run them after every laptop sleep, you forget which local port maps to which service, and you can’t tell at a glance whether a tunnel is even still up.

That’s where a client that treats tunnels as first-class state helps. Termalin keeps local, remote and dynamic forwards attached to the host as saved config — start them with a click, see which are live, and let them re-establish with the connection instead of retyping the command. The same host list (with its keys, jump hosts and keepalives) syncs across your machines end-to-end encrypted, and each server is verified on first connect. The command line teaches you how tunnels work; a client keeps you from rebuilding them by hand every morning.

Either way, the model is the whole trick: decide which side opens the port, and the rest of the flag writes itself.


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.

Try it on one host.

Termalin is a fast SSH client for you — and your agents.

Free tier · 14-day Pro trial · pricing