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
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
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
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
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
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
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
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
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
-
-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
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
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...
Or globally in sshd_config:
AllowTcpForwarding local
PermitOpen localhost:5432
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)