SSH Tunneling and Port Forwarding on Linux: -L, -R, -D and ProxyJump

ssh tunneling port forwarding - custom-ssht-featured.png

SSH does more than give you a shell. It can carry any TCP connection inside its encrypted channel, which lets you reach a private database,
expose a service from behind NAT, or browse through a remote network — all without a VPN. That power cuts both ways, so this guide also covers
the server-side settings that control it.

It builds on SSH key authentication and
OpenSSH hardening. For full network-level access rather than single ports, see the
WireGuard guide.

What Tunneling Means

A forward tells SSH to listen on one side of the connection and relay whatever arrives to a destination on the other side. Three flavours cover almost every need: local, remote and dynamic.

architecturessh-forwards.txt
  -L  local  : localhost:PORT  --ssh-->  destination reachable from the server
  -R  remote : server:PORT     --ssh-->  service reachable from your machine
  -D  dynamic: SOCKS proxy on localhost  --ssh-->  anywhere the server can reach
SSH tunnel concept
Traffic rides inside SSH

Local Forward (-L): Reach a Private Service

Open a port on your laptop that leads to a service only the bastion can see, such as a database on the internal network.

bashforward a database
ssh -N -L 5433:db.internal:5432 [email protected]
bashthen, in another terminal
psql -h 127.0.0.1 -p 5433 -U appuser appdb
Note-N means no remote command — the session only holds the tunnel open.
Local forward
localhost port to a private database

Remote Forward (-R): Expose a Service From Behind NAT

Publish a port on a remote server that leads back to something running on your machine. Handy for showing a dev site or reaching a home machine.

bashexpose local port 3000 as server port 9000
ssh -N -R 9000:localhost:3000 [email protected]
WarningBy default the remote port binds to the server’s loopback only. Exposing it publicly needs GatewayPorts on the server — think twice before enabling it.
Remote forward
Server port back to your app

Dynamic Forward (-D): A SOCKS Proxy

One tunnel, any destination. Point a browser or tool at the local SOCKS port and its traffic exits from the remote network.

bashstart the proxy
ssh -N -D 1080 [email protected]
bashtest it
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me
Dynamic forward
SOCKS5 through SSH

Reach Private Hosts with ProxyJump

When the target has no public address, hop through a bastion in one command. Your keys stay on your laptop; nothing is stored on the bastion.

bashone-shot
ssh -J [email protected] [email protected]
bashcopy a file the same way
scp -J [email protected] backup.tar.gz [email protected]:/tmp/
ProxyJump
Bastion in the middle

Store It in ~/.ssh/config

Give each destination an alias so the long flags disappear.

ini~/.ssh/config
Host bastion
    HostName bastion.example.com
    User user
    IdentityFile ~/.ssh/id_ed25519

Host appdb
    HostName 10.0.0.20
    User admin
    ProxyJump bastion
    LocalForward 5433 127.0.0.1:5432
    ServerAliveInterval 30
bashuse it
ssh appdb
ssh config
Aliases with ProxyJump and forwards

Keep a Tunnel Running with systemd

A tunnel that dies silently is worse than none. ExitOnForwardFailure makes SSH exit if the forward cannot be set up, and systemd restarts it.

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

[Service]
User=tunnel
ExecStart=/usr/bin/ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -L 5433:db.internal:5432 [email protected]
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
bashenable
sudo systemctl daemon-reload
sudo systemctl enable --now db-tunnel.service
Persistent tunnel
systemd with restart

Lock Forwarding Down on the Server

Anyone with SSH access can create tunnels unless you say otherwise. Disable forwarding globally and allow it only for the accounts and destinations that need it.

ini/etc/ssh/sshd_config
AllowTcpForwarding no
GatewayPorts no

Match User tunnel
    AllowTcpForwarding local
    PermitOpen db.internal:5432
    X11Forwarding no
    PermitTTY no
bashvalidate then reload
sudo sshd -t && sudo systemctl reload sshd
Restricting forwarding
AllowTcpForwarding and PermitOpen

Quick Reference

  • -L reach in, -R publish out, -D SOCKS, -J bastion hop
  • Persist with systemd; restrict with AllowTcpForwarding and PermitOpen

Related tutorials

Diagrams are original illustrations by Gnome IT Solutions. Tutorial text © Gnome IT Solutions.