DEV Community

Cover image for SSH Port Forwarding Explained: Local, Remote, and Dynamic Tunnels
FlatlinerAtheist | CLI-NERDS
FlatlinerAtheist | CLI-NERDS

Posted on Originally published at clinerds.com

SSH Port Forwarding Explained: Local, Remote, and Dynamic Tunnels

SSH is not just a remote shell. The same encrypted connection can carry other traffic inside it — databases, web panels, remote desktops — which means one open SSH port can safely unlock an entire private network. That feature is SSH port forwarding, and it replaces a surprising number of VPNs and exposed services.

This guide covers the three types of forwarding with commands you can copy, the flags that make tunnels usable in the real world, and the errors everyone hits.

The three kinds of SSH tunnel

  • Local forwarding (-L) — listen on your machine, forward through the server. "Let me reach something the server can reach."
  • Remote forwarding (-R) — listen on the server, forward back to you. "Let the server reach something on my machine."
  • Dynamic forwarding (-D) — a full SOCKS proxy through the server. "Route my browser's traffic through that machine."

All three encrypt whatever passes through them, which is the whole point: plaintext protocols like VNC, HTTP admin panels, or database connections travel inside an encrypted SSH channel.

Local forwarding: reach things behind the server

ssh -L local_port:destination_host:destination_port user@server
Enter fullscreen mode Exit fullscreen mode

Example 1 — a database that only listens on localhost. PostgreSQL on the server binds to 127.0.0.1:5432 and the firewall blocks everything else. Connect your local GUI client anyway:

ssh -L 5432:localhost:5432 user@db.example.com
Enter fullscreen mode Exit fullscreen mode

Now point pgAdmin or DBeaver at localhost:5432. The traffic between you and the server is encrypted, and port 5432 stays invisible to the internet.

Example 2 — a web panel on a private network. An internal dashboard runs on 10.0.0.15:8080, reachable only from the bastion host:

ssh -L 8080:10.0.0.15:8080 user@bastion.example.com
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8080 in your browser. Note the destination host is resolved by the server, which is why this works for machines you cannot reach directly.

Example 3 — remote desktop. RDP should never face the internet, so tunnel it:

ssh -L 3389:localhost:3389 user@meet.example.com
Enter fullscreen mode Exit fullscreen mode

Then connect your RDP client to localhost:3389. (Full setup in our XRDP on Debian guide.)

Remote forwarding: expose your local machine

ssh -R remote_port:local_host:local_port user@server
Enter fullscreen mode Exit fullscreen mode

Example — demo a local dev server. Your app runs on your laptop at localhost:3000, and you want a colleague (or a webhook provider like Stripe) to reach it:

ssh -R 8080:localhost:3000 user@vps.example.com
Enter fullscreen mode Exit fullscreen mode

Anyone hitting vps.example.com:8080 now lands on your laptop's port 3000.

By default the remote listener binds to the server's localhost only. To make it listen on all interfaces, the server's sshd_config needs:

GatewayPorts yes
Enter fullscreen mode Exit fullscreen mode

Think twice before enabling it — the tunnelled service becomes publicly reachable. A firewall rule restricting the port is strongly recommended.

Dynamic forwarding: instant SOCKS proxy

ssh -D 1080 user@server
Enter fullscreen mode Exit fullscreen mode

That single command creates a SOCKS5 proxy on your local port 1080. Point your browser or system proxy settings at localhost:1080 and all web traffic exits from the server. Useful on untrusted Wi-Fi, for reaching region-locked admin interfaces, or for browsing an internal network without a full VPN.

Configure Firefox manually (Settings → Network → SOCKS Host localhost, Port 1080, SOCKS v5, and tick "Proxy DNS when using SOCKS") so DNS lookups also travel through the tunnel.

Flags that make tunnels practical

ssh -L 5432:localhost:5432 -N -f user@server
Enter fullscreen mode Exit fullscreen mode
  • -N — do not open a shell; this connection exists only for forwarding
  • -f — go to the background after authenticating
  • -v — verbose; your first stop when a tunnel misbehaves

Tunnels die silently when connections idle out. Keep them alive:

ssh -L 5432:localhost:5432 -N \
  -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
  user@server
Enter fullscreen mode Exit fullscreen mode

For tunnels that must survive reboots and network drops, use autossh, which restarts the connection automatically:

sudo apt install autossh
autossh -M 0 -N \
  -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
  -L 5432:localhost:5432 user@server
Enter fullscreen mode Exit fullscreen mode

Wrap that in a small systemd unit and you have a persistent encrypted tunnel.

Locking down forwarding on the server

Forwarding is powerful, which also makes it abusable. A shared bastion should restrict it per key in authorized_keys:

restrict,port-forwarding,permitopen="localhost:5432" ssh-ed25519 AAAA...
Enter fullscreen mode Exit fullscreen mode

Or globally in sshd_config:

AllowTcpForwarding local
PermitOpen localhost:5432
Enter fullscreen mode Exit fullscreen mode

AllowTcpForwarding no disables tunnels entirely for users who only need a shell.

Troubleshooting the three classic errors

channel 3: open failed: connect failed: Connection refused — the SSH tunnel itself is up, but the destination refused the connection. Check that the target service is actually running and that you used the right destination host/port from the server's point of view.

bind: Address already in use — your local port is taken. Pick another local port (-L 5433:localhost:5432) or find the squatter with ss -tlnp | grep 5432.

Tunnel connects, then silently dies minutes later — NAT timeouts. Add the ServerAliveInterval options shown above, or switch to autossh.

When to use a VPN instead

SSH tunnels move one port (or SOCKS traffic) at a time. If you need full network access — routing all traffic, reaching many hosts, connecting printers and file shares — a real VPN like WireGuard is the right tool. But for "I just need that one database/admin panel/desktop," a five-second SSH command beats standing up a VPN every time.

Top comments (0)