SSH port forwarding explained: -L, -R and -D

Local, remote and dynamic SSH tunnels with examples you can paste: reach a database behind a bastion, expose a local port, or run a SOCKS proxy.

On this pageThe three kinds at a glanceLocal forwarding: -LRemote forwarding: -RDynamic forwarding: -DFlags worth knowingTunnels in ~/.ssh/configKeep tunnels safeWhen a tunnel does not workTunnels as cards in Gatesys SSHFAQ

SSH can carry any TCP connection inside its encrypted session. That one feature, port forwarding, replaces a good many VPNs and opened firewall ports: it reaches a database that only listens on a private network, shows a web console that is bound to localhost, or sends a browser’s traffic out through a server. There are three kinds. This guide shows each with a command you can paste, then the flags and the safety rules that matter.

The three kinds at a glance

FlagNameListens onConnects out fromTypical use
-LLocalYour computerThe serverReach a service on the server’s network
-RRemoteThe serverYour computerLet the server reach a service on your side
-DDynamicYour computerThe server, wherever each connection asksA SOCKS5 proxy through the server

Local forwarding: -L

ssh -L 15432:localhost:5432 deploy@db-primary

Read it as local port : destination host : destination port. While this session is open, anything that connects to port 15432 on your computer comes out on db-primary and is connected to localhost:5432 there. Point your client at it:

psql -h localhost -p 15432 -U app shop

Two things people get wrong:

  • The destination is resolved on the server. localhost in the command means the server, not your laptop. To reach another machine on the server’s network, name it: -L 16379:10.0.3.40:6379.
  • Pick a local port that is not the service’s own. If you forward to local 5432, a tool that meant to reach your development database reaches production instead. Adding 10000, so 15432 for PostgreSQL and 16379 for Redis, keeps them apart.

Remote forwarding: -R

ssh -R 8080:localhost:3000 deploy@staging

This is the mirror image. The server listens on its port 8080, and connections to it come out on your computer at localhost:3000. Use it to let a staging server call a webhook receiver that is running on your laptop.

By default the server listens on its loopback address only, so the port is reachable from the server itself and not from the internet. Making it public needs GatewayPorts yes in the server’s sshd_config, and a moment’s thought.

Dynamic forwarding: -D

ssh -D 1080 deploy@bastion

Now port 1080 on your computer is a SOCKS5 proxy. Each connection made through it is opened from the bastion, to wherever that connection asks. One tunnel reaches every internal web UI:

curl --socks5-hostname localhost:1080 http://grafana.internal:3000/

--socks5-hostname matters: it makes the bastion resolve the name, so internal DNS names work. In a browser, set a SOCKS5 proxy of localhost:1080 and turn on remote DNS.

Flags worth knowing

A tunnel on its own, with no shell, in the background:

ssh -fN -o ExitOnForwardFailure=yes -L 15432:localhost:5432 deploy@db-primary
FlagWhat it does
-NDo not run a command. The session exists only for the forward
-fGo to the background after logging in
-o ExitOnForwardFailure=yesFail at once if the local port is taken, instead of connecting with no tunnel
-o ServerAliveInterval=30Send a keepalive every 30 seconds, so idle tunnels are not dropped by a firewall

To stop a background tunnel cleanly, give it a control socket when you start it:

ssh -fNM -S ~/.ssh/pg.sock -L 15432:localhost:5432 deploy@db-primary
ssh -S ~/.ssh/pg.sock -O exit deploy@db-primary

Tunnels in ~/.ssh/config

A forward you use every day belongs in the config file:

Host db-tunnel
    HostName 10.0.3.4
    User deploy
    ProxyJump bastion
    LocalForward 15432 localhost:5432
    ExitOnForwardFailure yes
    ServerAliveInterval 30

Host proxy
    HostName bastion.example.com
    DynamicForward 1080

ssh -N db-tunnel then opens the tunnel through the bastion. The forward ends on the database host itself, and the bastion only relays encrypted bytes. See the ProxyJump guide.

Keep tunnels safe

  • Stay on loopback. -L 15432:... binds 127.0.0.1. Writing -L 0.0.0.0:15432:... offers your production database to everyone on the café’s network.
  • Treat a tunnel to production as production. A forwarded port has no idea that it should be read-only. Open a read-only session where the service has one, for example PGOPTIONS="-c default_transaction_read_only=on" psql ....
  • Close what you opened. A forgotten -f tunnel keeps working for as long as the laptop is awake.
  • Limit forwarding on the server. In sshd_config, AllowTcpForwarding takes yes, no, local or remote. PermitOpen limits where local forwards may go, and PermitListen limits where remote forwards may listen.

When a tunnel does not work

SymptomCause
bind [127.0.0.1]:15432: Address already in useAnother program, often an older tunnel, holds the local port
The tunnel starts, then channel 2: open failed: connect failed: Connection refused when you use itNothing listens on the destination. SSH checks the destination only when a connection arrives, not when the tunnel starts
open failed: administratively prohibitedThe server’s AllowTcpForwarding or PermitOpen forbids it
Warning: remote port forwarding failed for listen port 8080The port is taken on the server, or it is under 1024 and you are not root
The tunnel dies after a few idle minutesA firewall dropped the idle connection. Set ServerAliveInterval

The second row is worth remembering. ssh -L succeeds even when the far end is closed. You find out on the first connection.

Tunnels as cards in Gatesys SSH

In Gatesys SSH, port forwarding is a view of its own, on the free plan. Each tunnel is a card with a switch, and it rides the host’s existing session, jump chain included.

  • Describe it. Type reach postgres on db-primary, or paste ssh -L 15432:localhost:5432 db-primary, and a draft card appears with every value labelled by where it came from.
  • It is tested when it starts. The app makes one test connection through the tunnel, and the card says Running only if that gets through. Otherwise it says why, for example db-primary refused 127.0.0.1:5432 — nothing listening?
  • Safe by default. Tunnels listen on 127.0.0.1 only. A data service gets its port plus 10000, so a production database never answers on your local database’s port. On a production host, the suggested client command opens a read-only session first.
  • On the record. Every start and stop is written to the audit log with its host.

FAQ

What is the difference between ssh -L and ssh -R?

-L listens on your computer and connects out from the server. -R listens on the server and connects out from your computer. Use -L to reach something on the server’s side, and -R to let the server reach something on yours.

Is SSH port forwarding secure?

The traffic between your computer and the SSH server is encrypted and authenticated. The last leg, from the server to the destination, is whatever that service speaks. The main risk is exposure: a tunnel bound to all interfaces is open to your whole network.

Can SSH forward UDP?

No. -L, -R and -D carry TCP connections, and Unix domain sockets. For UDP you need a VPN, or a tool that wraps UDP in TCP at both ends.

How do I keep an SSH tunnel alive?

Set ServerAliveInterval 30 so the idle connection is not dropped. For a tunnel that must come back by itself after a network change, run it under autossh or a systemd service with Restart=always.

Why does my tunnel connect but the port refuses?

The destination is not listening where you pointed. Remember that the destination is resolved on the server: localhost:5432 works only if the database listens on the server’s loopback address.

Keep reading

All posts
Guides

SSH ProxyJump: a practical guide to jump hosts

Reach servers behind a bastion with ssh -J and ProxyJump: config examples, multi-hop chains, scp and rsync through a jump host, and bastion hardening.

5 min read

Guides

Debugging a failed ProxyJump chain, hop by hop

An SSH connection through a bastion can fail at five steps on every hop. How to read the error, find the hop that broke, and test from the hop in front of it.

6 min read

Your servers, understood.

Download Gatesys SSH free. Start Pro when you want Farabi — the first 3 months are on us.