Guides

SSH tunnel explained: local, remote and dynamic (SOCKS) port forwarding

What an SSH tunnel is and how to build one: local forwarding with ssh -L, reverse tunnels with ssh -R, a SOCKS proxy with ssh -D, autossh and PuTTY.

Published ~12 min read

Short answer: an SSH tunnel carries another network connection inside an encrypted SSH session. You connect to a server you can already log in to, and SSH forwards a port through that connection: ssh -L brings a port of the server (or of a host behind it) to your computer, ssh -R does the reverse and publishes a port of your computer on the server, and ssh -D turns the connection into a SOCKS proxy. Nothing but the SSH port has to be open in the firewall.

the three kinds, at a glance
# local:   your 127.0.0.1:3307  ->  the server's 127.0.0.1:3306
ssh -N -L 3307:127.0.0.1:3306 you@203.0.113.10

# remote:  the server's 127.0.0.1:8080  ->  your 127.0.0.1:3000
ssh -N -R 8080:127.0.0.1:3000 you@203.0.113.10

# dynamic: a SOCKS proxy on your 127.0.0.1:1080, exiting at the server
ssh -N -D 1080 you@203.0.113.10

What is an SSH tunnel?

An SSH connection is an encrypted channel between your computer and a server, usually on TCP port 22. Besides a shell, the protocol can carry other TCP connections through that same channel. That is port forwarding, and the forwarded connection is what people call an SSH tunnel: an application talks to a port on one end, and SSH delivers the bytes to a port on the other end.

Three things follow from that, and they explain why tunnels are so common on a VPS:

  • A service can stay on localhost. A database, an admin panel or a remote desktop that listens on 127.0.0.1 only cannot be reached from the internet at all, yet you can use it from your own machine through the tunnel.
  • The traffic is encrypted between your computer and the server, even if the protocol inside (plain HTTP, VNC, an old database driver) is not.
  • Only the SSH port is exposed. Your firewall keeps everything else closed, and access to the service is as strong as your SSH login — keys rather than passwords, as in our SSH hardening guide.

The tunnel only exists while the SSH session does, and only for the port you forwarded. It is not a VPN: the rest of your computer’s traffic is not affected.

Local port forwarding with ssh -L

Local forwarding is the one you will use most. It opens a port on your computer, and whatever connects to it is carried to the server and handed on from there:

syntax
ssh -L [bind_address:]local_port:destination:destination_port user@server

The important detail is that destination is resolved on the server. So 127.0.0.1 means the server itself, and a private address such as 10.0.0.5 means a machine the server can reach and you cannot. A MariaDB server that listens on the VPS’s loopback interface only:

on your computer
ssh -N -L 3307:127.0.0.1:3306 you@203.0.113.10

# in a second terminal, or in your database client:
mysql -h 127.0.0.1 -P 3307 -u appuser -p
  • -N tells ssh not to start a shell: the session exists only for the tunnel. Add -f to send it to the background once you have logged in.
  • Port 3307 on your side is arbitrary. Pick one that is free; reusing 3306 fails if a database runs on your computer too.
  • Use 127.0.0.1 rather than localhost in the client. Many MySQL and MariaDB clients treat localhost as “use the local socket file” and never touch the tunnel.
  • The forwarded port listens on your loopback interface only. Other machines on your network can use it only if you name a bind address in front (-L 0.0.0.0:3307:…) — rarely what you want.

What people tunnel to on a VPS

ServicePort on the serverExampleGuide
MariaDB / MySQL3306-L 3307:127.0.0.1:3306Install MariaDB or MySQL
PostgreSQL5432-L 5433:127.0.0.1:5432Install PostgreSQL
phpMyAdmin80-L 8080:127.0.0.1:80phpMyAdmin, SSH-only
Windows Remote Desktop3389-L 13389:127.0.0.1:3389RDP behind SSH
xrdp (Linux desktop)3389-L 3390:127.0.0.1:3389xrdp on Ubuntu and Debian
VNC5901-L 5901:127.0.0.1:5901RDP vs VNC
Monero node RPC18081-L 18081:127.0.0.1:18081Monero node on a VPS
FiveM txAdmin40120-L 40120:127.0.0.1:40120FiveM server
Rust RCON28016-L 28016:127.0.0.1:28016Rust server hosting
7 Days to Die web dashboard8080-L 8080:127.0.0.1:80807 Days to Die server

You can forward several ports in one session by repeating -L, for example -L 3307:127.0.0.1:3306 -L 8080:127.0.0.1:80.

A shell on a machine behind the server: ssh -J

When you want to log in to a host that only the server can reach, not just use one of its ports, let SSH hop through the server with -J (ProxyJump, OpenSSH 7.3 and later). The inner connection is encrypted end to end inside the outer one, and your private key never leaves your computer:

on your computer
ssh -J you@203.0.113.10 admin@10.0.0.5

In ~/.ssh/config, the same hop is a ProxyJump you@203.0.113.10 line under the inner host. It rides on the same mechanism as -L, so the jump server has to allow local forwarding (see what the server has to allow).

Remote port forwarding with ssh -R (reverse SSH tunnel)

Remote forwarding turns the direction around. The server opens a port, and connections to it travel back through the session to your computer — or to a machine your computer can reach:

syntax
ssh -R [bind_address:]server_port:destination:destination_port user@server

Here destination is resolved on your side. Two typical uses: reaching a machine that sits behind NAT or a firewall that allows no incoming connections, and showing a web app that runs on your laptop to someone else.

on the machine behind NAT
# publish this machine's SSH (port 22) as port 2222 on the VPS
ssh -N -R 2222:127.0.0.1:22 you@203.0.113.10

# later, on the VPS:
ssh -p 2222 user@127.0.0.1

By default sshd binds a remote forward to the server’s loopback interface, so only people logged in to the server can use it. To make the port reachable from outside, the server needs GatewayPorts set in /etc/ssh/sshd_config: clientspecified lets the client choose with -R 0.0.0.0:8080:127.0.0.1:3000, yes binds every remote forward to all interfaces. Open that port in the server’s firewall too, and think about who can reach it: whatever you publish this way is on the internet.

Dynamic port forwarding: ssh -D as a SOCKS proxy

With -D, SSH does not forward one fixed port. It runs a SOCKS proxy on your computer, and every connection an application makes through that proxy leaves from the server, to whatever address the application asked for:

on your computer
ssh -N -D 1080 you@203.0.113.10

# test it: the address shown is the server's, not yours
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me

Point a browser at the proxy — in Firefox under Settings, Network Settings, “Manual proxy configuration”, SOCKS host 127.0.0.1, port 1080, SOCKS v5 — and tick “Proxy DNS when using SOCKS v5” so that name lookups go through the tunnel as well. In curl, --socks5-hostname does the same, where --socks5 would resolve names locally. With the proxy set, What is my IP should show the server’s address instead of yours.

Put tunnels in ~/.ssh/config

Typing forwarding options every time gets old. The same three kinds have config directives, so a short host alias opens the whole set:

~/.ssh/config
Host db-tunnel
    HostName 203.0.113.10
    User you
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 3307 127.0.0.1:3306
    LocalForward 8080 127.0.0.1:80
    # RemoteForward 2222 127.0.0.1:22
    # DynamicForward 1080
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

Then ssh -N db-tunnel is all it takes. If your server listens on a port other than 22, add a Port line — our SSH port guide shows how to find out which one it uses.

To check what an alias will forward before you rely on it, let ssh print its resolved configuration. Nothing connects:

Linux · macOS
ssh -G db-tunnel | grep -E '^(localforward|exitonforwardfailure) '
Example output
exitonforwardfailure yes
localforward 3307 [127.0.0.1]:3306
localforward 8080 [127.0.0.1]:80

Keep a tunnel open: ServerAliveInterval, autossh and systemd

An idle tunnel dies when a NAT router or a firewall forgets the connection. ServerAliveInterval 30 sends a keep-alive through the encrypted channel every 30 seconds, and ServerAliveCountMax 3 ends the session after three unanswered ones instead of leaving a dead tunnel in place. ExitOnForwardFailure yes makes ssh quit when it cannot set up the forward at all, rather than sitting there without it.

For a tunnel that has to come back on its own after a network outage, use autossh. It starts ssh and restarts it whenever it exits:

Debian · Ubuntu
sudo apt install -y autossh

autossh -M 0 -N \
  -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -L 3307:127.0.0.1:3306 you@203.0.113.10

-M 0 switches off autossh’s own monitoring port and lets the keep-alives above detect a dead connection. A tunnel that has to survive a reboot belongs in a systemd unit, with a key that needs no passphrase and is used for nothing else:

/etc/systemd/system/db-tunnel.service
[Unit]
Description=SSH tunnel to the database server
After=network-online.target
Wants=network-online.target

[Service]
User=tunnel
Environment=AUTOSSH_GATETIME=0
ExecStart=/usr/bin/autossh -M 0 -N \
  -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=yes \
  -i /home/tunnel/.ssh/id_ed25519 \
  -L 3307:127.0.0.1:3306 tunnel@203.0.113.10
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
enable it
sudo systemctl daemon-reload
sudo systemctl enable --now db-tunnel
systemctl status db-tunnel

AUTOSSH_GATETIME=0 keeps autossh retrying even if the very first connection fails, which is what you want at boot. Create the key as that user, install it on the server with ssh-copy-id, and log in once by hand, so the server’s host key is in the user’s known_hosts. More on writing units in our systemd service guide.

Stop a tunnel running in the background

A tunnel in the foreground ends with Ctrl+C. One started with -f has no window to close. On Linux and macOS, give it a control socket when you start it:

Linux · macOS
ssh -f -N -M -S ~/.ssh/db-tunnel.sock -L 3307:127.0.0.1:3306 you@203.0.113.10

-M makes this ssh the master of the socket named by -S. Through that socket you can ask whether the tunnel is still up, and end it:

Linux · macOS
ssh -S ~/.ssh/db-tunnel.sock -O check you@203.0.113.10
ssh -S ~/.ssh/db-tunnel.sock -O exit you@203.0.113.10

The first answers Master running (pid=…), the second Exit request sent. A tunnel started without a socket is an ordinary process: pgrep -af 'ssh.*-L 3307' shows its process ID for kill.

SSH tunnels on Windows: OpenSSH and PuTTY

Windows 10 and 11 ship the OpenSSH client, so every command on this page works as it is in PowerShell or the command prompt: ssh -N -L 3307:127.0.0.1:3306 you@203.0.113.10. Your config file lives in C:\Users\you\.ssh\config.

In PuTTY, a tunnel is part of a saved session:

  • Enter the server under Session as usual.
  • Go to Connection → SSH → Tunnels. For a local forward, type the local port under Source port (3307) and 127.0.0.1:3306 under Destination, leave Local selected and click Add. For a reverse tunnel pick Remote; for a SOCKS proxy pick Dynamic and leave the destination empty.
  • Back under Session, click Save, then Open. The tunnel lives as long as that PuTTY window.

PuTTY’s command-line companion does the same without a window: plink -N -L 3307:127.0.0.1:3306 you@203.0.113.10.

What the server has to allow

On a stock OpenSSH server, local and remote forwarding are allowed and remote forwards bind to loopback. These sshd_config settings change that:

SettingDefaultWhat it does
AllowTcpForwardingyesyes / all, local, remote or no: which kinds of forwarding users may open
GatewayPortsnoWhether remote forwards may listen on other addresses than loopback (yes, clientspecified)
PermitOpenanyDestinations a local forward may connect to, e.g. 127.0.0.1:3306
PermitListenanyPorts a remote forward may listen on (OpenSSH 7.8 and later)

For a user that exists only to hold a tunnel — the tunnel user of the systemd example — lock the account down to exactly that:

end of /etc/ssh/sshd_config
Match User tunnel
    AllowTcpForwarding local
    PermitOpen 127.0.0.1:3306
    X11Forwarding no
    AllowAgentForwarding no
    PermitTTY no
    ForceCommand /bin/false
check and reload
sudo sshd -t && sudo systemctl reload ssh     # the unit is sshd on Rocky and Alma

When the tunnel does not work

  • bind [127.0.0.1]:3307: Address already in use — something on your computer already listens on that local port, often an earlier tunnel. Pick another port, or find the process as in Address already in use.
  • channel 2: open failed: connect failed: Connection refused — the tunnel is up, but nothing on the server listens at the destination you gave. Check the address and port with sudo ss -tlnp on the server: a database bound to the public IP only is not reachable at 127.0.0.1.
  • open failed: administratively prohibited: open failed — the server’s AllowTcpForwarding or PermitOpen forbids this forward.
  • Warning: remote port forwarding failed for listen port 8080 — that port is already taken on the server, or PermitListen does not allow it.
  • The tunnel opens but every page stays blank — you connected the client to the wrong end. With -L the client talks to 127.0.0.1 on your computer, on the local port.
  • No SSH connection at all — that is not a tunnel problem yet. Start with SSH connection refused.

Frequently asked

What is an SSH tunnel?

A network connection carried inside an encrypted SSH session. SSH forwards a port through the session: with ssh -L a port of the server comes to your computer, with ssh -R a port of your computer appears on the server, and with ssh -D the session becomes a SOCKS proxy. Only the SSH port has to be open in the firewall.

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

ssh -L opens the port on your computer and connects to the destination from the server's side — the way to reach a database or an admin panel that listens only on the server. ssh -R opens the port on the server and connects back to your side — the way to reach a machine behind NAT or to show someone an app running on your laptop.

Is an SSH tunnel a VPN?

No. A tunnel forwards the ports you name, and ssh -D proxies only the applications you point at it. A VPN such as WireGuard routes all of a device's traffic. For one database or one admin panel a tunnel is simpler; for everything a device sends, use a VPN.

Why does my SSH tunnel close after a few minutes?

A router or firewall on the way drops the idle connection. Add -o ServerAliveInterval=30 -o ServerAliveCountMax=3 so that SSH sends keep-alives, and run a tunnel that must come back on its own under autossh or a systemd service.

Related articles

All guides