Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

SSH tunnels: -L, -R, -D and ProxyJump

By updated

Improve this page

An SSH tunnel carries a network flow through an encrypted SSH connection. As a result, you reach an internal database as if it were local, you expose a development service on a public server, or you send all your traffic through a remote server. No need for a VPN, no extra port to open.

It extends the LFCS objective "Configure the OpenSSH server and client" (Networking domain). The three kinds of tunnel were replayed on Debian 13 with OpenSSH 10.0: -L and -D serve an internal web page, -R does listen on the loopback by default.

What you will learn

  • Create a local tunnel (-L) to reach a service behind a bastion
  • Create a remote tunnel (-R) to make a local service reachable from outside
  • Create a dynamic tunnel (-D) as a SOCKS proxy for all traffic
  • Use ProxyJump to go through one or several intermediate hosts
  • Persist tunnels in ~/.ssh/config
  • Diagnose a blocked tunnel

In which context?

SSH tunnels help when the network prevents direct access: a database on a private network unreachable from outside, an internal web service only visible from a bastion, a local development environment to share temporarily with a colleague. In one command, SSH creates an encrypted channel that gets around these restrictions without changing the target server's firewall rules.


Local tunnel (-L): reach a remote service

A local tunnel forwards a port of your machine to a port on (or reachable from) the SSH server.

Syntax:

ssh -L [local_address:]local_port:target_host:target_port user@bastion

Example: reach MySQL behind a bastion

You want to connect to a MySQL server on 192.168.1.100:3306, only reachable from the bastion bastion.example.com:

Terminal window
ssh -L 3307:192.168.1.100:3306 bob@bastion.example.com

Then, from your local machine:

Terminal window
mysql -h 127.0.0.1 -P 3307 -u root -p

MySQL answers as if it were local. The traffic travels in clear text from the MySQL client to localhost:3307, then encrypted by SSH up to the bastion, and in clear text again from the bastion to 192.168.1.100:3306.

Tunnel in the background

A tunnel opened in a terminal disappears as soon as that terminal closes. The -N and -f options detach the ssh process from the current shell: control returns immediately and the tunnel survives closing the window. The downside: no error message is shown any more, so establish the tunnel in the foreground first to validate that it works, then start it again detached.

Terminal window
ssh -N -f -L 3307:192.168.1.100:3306 bob@bastion.example.com
  • -N: do not open a shell, just the tunnel
  • -f: go to the background before running

To close the tunnel:

Terminal window
# Find the PID, without the search counting itself
pgrep -af "ssh -N"
kill <PID>

Persistence in ~/.ssh/config

Typing the full forward at every connection ends up producing port mistakes. The LocalForward directive in ~/.ssh/config freezes the forward in the host's profile: the syntax there separates the local port and the target with a space, where the command line joins them with a colon. The file must be writable by no one but its owner, otherwise OpenSSH refuses to use it; 600 is the usual value.

Host bastion-mysql
HostName bastion.example.com
User bob
LocalForward 3307 192.168.1.100:3306

Then simply:

Terminal window
ssh -N bastion-mysql

Remote tunnel (-R): expose a local service

A remote tunnel forwards a port on the SSH server to a port of your local machine. It is the reverse of the local tunnel.

Syntax:

ssh -R [remote_address:]remote_port:local_host:local_port user@server

Example: share a local development server

You have a development server on localhost:8080 and want to make it reachable through server.example.com:8080:

Terminal window
ssh -R 8080:localhost:8080 bob@server.example.com

Anyone who opens http://server.example.com:8080 sees your local server.


Dynamic tunnel (-D): SOCKS proxy

A dynamic tunnel turns the SSH connection into a SOCKS proxy. Instead of forwarding a single port to a fixed target, all the traffic sent to the proxy is routed through the SSH server, which decides the destination.

Syntax:

Terminal window
ssh -D 1080 bob@bastion.example.com

Then configure your application to use localhost:1080 as a SOCKS5 proxy.

Chromium from the command line:

Terminal window
chromium --proxy-server="socks5://localhost:1080"

Firefox accepts no proxy option on the command line: the setting goes through about:preferences, Network Settings section, choosing "Manual proxy configuration" then filling in the SOCKS host localhost and port 1080. On that same screen, the option that delegates DNS resolution to the SOCKS v5 proxy (preference network.proxy.socks_remote_dns) is essential: without it, internal names are resolved by your local resolver, which does not know them.

curl with SOCKS:

Terminal window
curl --socks5 localhost:1080 http://internal-service.example.com/api/status

ProxyJump: go through several hosts

ProxyJump (-J) establishes the connection through one or several intermediate hosts. It is the modern method, simpler to write than ProxyCommand (which it replaces with a single option), and it is an implicit tunnel.

Simple case: bastion → target

The important point is that the final authentication happens against the target, not against the bastion. The bastion is only a TCP relay: it only sees end-to-end encrypted traffic between your workstation and 10.0.1.15. An administrator of the bastion therefore cannot read your session, unlike a chain of two ssh where the second starts from the bastion.

Terminal window
ssh -J bob@bastion.example.com alice@10.0.1.15

Multiple hops

Hosts are declared in traversal order, separated by commas, and the last argument remains the final destination. Each hop opens a new nested session: the latency adds up at each level, and each host must know your public key. If an intermediate hop fails, the error message names the faulty host, which lets you locate the break without testing each machine by hand.

Terminal window
ssh -J bob@bastion.example.com,carol@10.0.1.5 alice@10.0.2.20

Combine ProxyJump and a local tunnel

-J and -L combine without conflict because they act at two levels: -J builds the access path, -L opens the forward once there. The trap lies in the forward's localhost: it designates 10.0.2.20, the final host, not your workstation. The -N avoids opening a useless shell on the production machine.

Terminal window
# Reach the database on 10.0.2.20 through two intermediate hosts
ssh -J bob@bastion.example.com,carol@10.0.1.5 \
-L 5432:localhost:5432 \
alice@10.0.2.20 -N

In ~/.ssh/config

The same chain written once in the configuration file replays with ssh -N prod-db. The ProxyJump directive accepts the list of hops with the same syntax as the command-line option, and LocalForward applies to the final host. This form is the one to favour as soon as an access is repeated: the tools that rely on SSH, such as scp, rsync or git, read the same file and automatically inherit the path.

Host prod-db
HostName 10.0.2.20
User alice
ProxyJump bob@bastion.example.com,carol@10.0.1.5
LocalForward 5432 localhost:5432

Persistent tunnels with autossh

For a tunnel that reconnects automatically if the connection is lost:

Terminal window
# Installation
sudo apt install autossh # Debian/Ubuntu
sudo dnf install autossh # RHEL/Fedora
# Persistent local tunnel
autossh -M 0 -N -f \
-o "ServerAliveInterval 30" \
-o "ServerAliveCountMax 3" \
-L 3307:192.168.1.100:3306 \
bob@bastion.example.com

-M 0 turns off autossh's monitoring port and delegates break detection to the ServerAlive options.


Troubleshooting

The reflex that saves the most time: run the command again with -v, or even -vvv. The output shows at which step the tunnel breaks, and the distinction is decisive. A message emitted by the client signals a local problem (port taken, key refused); a message prefixed channel N: comes from the SSH server and points to a reachability problem on the remote side. Read the table with this grid: the "Likely cause" column tells you on which side of the tunnel to look.

SymptomLikely causeAction
bind [127.0.0.1]:9001: Address already in useLocal port already usedChange the local port or stop the process that holds it (ss -tlnp)
Tunnel up but connection refusedGatewayPorts no on the server sideAdd GatewayPorts yes in an sshd_config.d/ drop-in (-R tunnel)
-D tunnel: applications do not go throughSOCKS proxy wrongly configured in the appCheck that the app uses SOCKS5, not SOCKS4 or HTTP
Tunnel closes after a few minutesSSH timeout on the server sideAdd ServerAliveInterval 60 in ~/.ssh/config or use autossh
channel 2: open failed: connect failedThe tunnel's target server is unreachable from the bastionCheck that 192.168.1.100:3306 is reachable from the bastion, not from your machine
ProxyJump: Permission deniedPublic key missing from the authorized_keys of a host of the chainDeploy the public key on each host crossed; each hop authenticates from your workstation, ForwardAgent is useless

Key points

  • -L local_port:target:target_port: reach a remote service as if it were local
  • -R remote_port:target:target_port: expose a local service on the SSH server, on its loopback unless GatewayPorts says otherwise
  • -D port: SOCKS proxy for all of an application's traffic
  • -N -f: tunnel without a shell, in the background
  • ProxyJump replaces ProxyCommand and supports multiple hops, each authenticated from your workstation
  • For critical tunnels, autossh ensures automatic reconnection

Knowledge check

Check that the essentials of this guide stuck. The questions cover only what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

FAQ: frequently asked questions

The most frequent questions about SSH tunnels, from choosing between -L, -R and -D to persistence with autossh.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn