SSH tunneling on Linux creates encrypted channels that forward network traffic between machines, letting you access services behind firewalls, encrypt otherwise unprotected protocols, and build secure paths through untrusted networks. Whether you need to reach a database on a private network, expose a local development server to a colleague, or route your browser traffic through a trusted exit point, SSH port forwarding handles all of these with tools already installed on every Linux system. This guide covers all three forwarding types with production-tested configurations.

Prerequisites

  • Two Linux machines with SSH access (or one Linux client and any SSH server)
  • OpenSSH client installed locally (ssh -V to check)
  • OpenSSH server running on the remote machine (sshd)
  • Basic understanding of TCP ports and client-server networking
  • SSH key authentication configured (recommended — see related article)

Understanding SSH Tunnel Types

SSH supports three distinct forwarding modes. Each serves a different use case:

TypeFlagDirectionUse Case
Local forwarding-LLocal machine → remote destinationAccess a remote database, web UI, or service
Remote forwarding-RRemote server → local machineExpose local dev server, allow remote access to local services
Dynamic forwarding-DLocal SOCKS proxy → any destination via remoteSecure web browsing, route all traffic through SSH

All three encrypt traffic end-to-end between your SSH client and the SSH server. Traffic between the SSH server and the final destination is not encrypted by SSH (it travels on the server’s local network).

Local Port Forwarding (-L)

Local forwarding is the most common type. It binds a port on your local machine and sends traffic through the SSH tunnel to a specific destination reachable from the remote server.

Syntax

ssh -L [bind_address:]local_port:destination:dest_port user@ssh_server

Example: Access a Remote Database

You have a PostgreSQL database on db.internal port 5432 that is only accessible from the application server app.example.com. Your laptop cannot reach db.internal directly.

# Forward local port 5432 to db.internal:5432 through app.example.com
ssh -L 5432:db.internal:5432 admin@app.example.com

Now connect to the database locally:

psql -h localhost -p 5432 -U myuser mydb

Your traffic path: laptop:5432 → SSH tunnel → app.example.com → db.internal:5432

Example: Access a Remote Web Interface

A monitoring dashboard runs on http://monitor.internal:3000 behind a firewall:

# Forward local port 8080 to the remote dashboard
ssh -L 8080:monitor.internal:3000 admin@jumpbox.example.com

# Open in your browser
# http://localhost:8080

Background Tunnel (No Shell)

Add -f -N to run the tunnel in the background without opening a remote shell:

# -f = background after authentication
# -N = no remote command (tunnel only)
ssh -f -N -L 8080:monitor.internal:3000 admin@jumpbox.example.com

To close a background tunnel, find and kill the process:

ps aux | grep 'ssh -f -N -L'
kill <PID>

Bind to All Interfaces

By default, -L binds to localhost only. To allow other machines on your network to use the tunnel:

# Bind to all interfaces (0.0.0.0)
ssh -L 0.0.0.0:8080:monitor.internal:3000 admin@jumpbox.example.com

# Or bind to a specific interface
ssh -L 192.168.1.100:8080:monitor.internal:3000 admin@jumpbox.example.com

Security warning: Binding to 0.0.0.0 exposes the tunnel to your entire local network. Only do this on trusted networks.

Remote Port Forwarding (-R)

Remote forwarding works in the opposite direction — it binds a port on the remote server and forwards traffic back to your local machine.

Syntax

ssh -R [bind_address:]remote_port:destination:dest_port user@ssh_server

Example: Expose a Local Development Server

You are developing a web application on localhost:3000 and want a colleague on the remote server to access it:

# Bind port 9000 on the remote server, forwarding to your local port 3000
ssh -R 9000:localhost:3000 admin@remote.example.com

Now anyone on remote.example.com can access your local dev server at http://localhost:9000.

Example: Provide Remote Access to a Local Service

Your office machine runs a service on port 8443 and you want to access it from home through a cloud server:

# From office machine:
ssh -R 8443:localhost:8443 user@cloud-server.example.com

From home, SSH into the cloud server and access localhost:8443.

Allowing External Connections

By default, remote forwarded ports bind to localhost on the remote server. To allow external access, the SSH server must have GatewayPorts enabled:

# On the SSH server, edit /etc/ssh/sshd_config:
GatewayPorts yes        # Allow binding to all interfaces
# or
GatewayPorts clientspecified  # Let the client choose the bind address

Then restart sshd and specify the bind address:

ssh -R 0.0.0.0:9000:localhost:3000 admin@remote.example.com

Dynamic Port Forwarding (-D) — SOCKS Proxy

Dynamic forwarding creates a local SOCKS5 proxy. Any application configured to use this proxy will route all its traffic through the SSH tunnel, with the remote server acting as the exit point.

Syntax

ssh -D [bind_address:]port user@ssh_server

Example: Secure Web Browsing

You are on an untrusted Wi-Fi network and want to route all browser traffic through your home server:

# Create a SOCKS5 proxy on local port 1080
ssh -D 1080 -f -N user@home-server.example.com

Then configure your browser:

  • Firefox: Settings → Network Settings → Manual proxy → SOCKS Host: localhost, Port: 1080, SOCKS v5. Check “Proxy DNS when using SOCKS v5”.
  • Chrome (command line): google-chrome --proxy-server="socks5://localhost:1080"
  • System-wide (environment variable): export ALL_PROXY=socks5://localhost:1080

Example: Route CLI Tools Through the Proxy

# Use with curl
curl --proxy socks5h://localhost:1080 https://ifconfig.me

# Use with git
git -c http.proxy=socks5h://localhost:1080 clone https://github.com/user/repo.git

# The 'h' in socks5h means DNS resolution happens on the remote side

Important: Use socks5h:// (with the h) to resolve DNS through the proxy. Plain socks5:// resolves DNS locally, which leaks your DNS queries on the local network.

Making Tunnels Persistent with autossh

SSH tunnels die when the connection drops (network interruption, laptop sleep, server reboot). autossh monitors the connection and reconnects automatically.

Installation

# Debian / Ubuntu
sudo apt install autossh

# RHEL / Fedora
sudo dnf install autossh

# Arch Linux
sudo pacman -S autossh

Usage

# Persistent local forward with autossh
autossh -M 0 -f -N -L 5432:db.internal:5432 admin@jumpbox.example.com

# Persistent SOCKS proxy
autossh -M 0 -f -N -D 1080 user@home-server.example.com

# Persistent remote forward
autossh -M 0 -f -N -R 9000:localhost:3000 admin@remote.example.com

The -M 0 flag disables autossh’s built-in monitoring port and relies on SSH’s own keepalive mechanism instead (more reliable and simpler). Pair it with SSH keepalives in your config.

Run as a systemd Service

For tunnels that must survive reboots, create a systemd unit:

# /etc/systemd/system/ssh-tunnel-db.service
[Unit]
Description=SSH Tunnel to Database
After=network-online.target
Wants=network-online.target

[Service]
User=tunneluser
ExecStart=/usr/bin/autossh -M 0 -N -L 5432:db.internal:5432 admin@jumpbox.example.com
Restart=always
RestartSec=10

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

SSH Config for Reusable Tunnels

Instead of typing long commands, define tunnels in ~/.ssh/config:

Host db-tunnel
    HostName jumpbox.example.com
    User admin
    LocalForward 5432 db.internal:5432
    ServerAliveInterval 60
    ServerAliveCountMax 3
    IdentityFile ~/.ssh/id_ed25519

Host socks-proxy
    HostName home-server.example.com
    User myuser
    DynamicForward 1080
    ServerAliveInterval 60
    ServerAliveCountMax 3
    IdentityFile ~/.ssh/id_ed25519

Host expose-dev
    HostName remote.example.com
    User admin
    RemoteForward 9000 localhost:3000
    ServerAliveInterval 60
    ServerAliveCountMax 3

Now use them by name:

ssh -f -N db-tunnel
ssh -f -N socks-proxy
ssh -f -N expose-dev

Comparing SSH Tunneling with Alternatives

FeatureSSH TunnelingWireGuard VPNCloudflare Tunnelngrok
EncryptionYes (SSH)Yes (WireGuard)Yes (TLS)Yes (TLS)
Requires server softwaresshd (ubiquitous)WireGuard kernel modulecloudflared daemonngrok agent
Per-port forwardingYesNo (full network)YesYes
SOCKS proxyYes (-D)NoNoNo
Speed overheadModerate (TCP-over-TCP)Low (UDP, kernel-space)LowLow
Persistent reconnectionWith autosshBuilt-inBuilt-inBuilt-in
CostFreeFreeFree tier availableFree tier available
Best forQuick ad-hoc tunnels, accessing specific servicesSite-to-site or full network accessExposing services to the internetQuick demos, webhooks

Use SSH tunneling when you need to quickly access one or two specific ports and already have SSH access — no additional software needed. Switch to WireGuard when you need full network access or high throughput. Use Cloudflare Tunnels or ngrok when you need to expose services to external users with public URLs.

Troubleshooting SSH Tunnels

Tunnel connects but no traffic flows

# Verify the tunnel is listening
ss -tlnp | grep <local_port>

# Check if the destination is reachable from the SSH server
ssh admin@jumpbox.example.com "nc -zv db.internal 5432"

If ss shows the port listening but connections hang, the destination service may be rejecting connections from the SSH server’s IP. Check firewall rules on the destination host.

”bind: Address already in use"

# Find what is using the port
sudo ss -tlnp | grep <port>

# Kill the old tunnel or choose a different local port
ssh -L 15432:db.internal:5432 admin@jumpbox.example.com

"channel 3: open failed: administratively prohibited”

The SSH server has TCP forwarding disabled. The server admin needs to set in /etc/ssh/sshd_config:

AllowTcpForwarding yes
# or for local forwarding only:
AllowTcpForwarding local

Tunnel disconnects after idle period

Add keepalives to your ~/.ssh/config:

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

This sends a keepalive every 60 seconds and disconnects after 3 missed responses (3 minutes). It prevents NAT devices and stateful firewalls from dropping the idle TCP connection.

Remote forwarding port not accessible externally

Check that GatewayPorts is enabled on the SSH server:

grep GatewayPorts /etc/ssh/sshd_config

If it says no or is absent, remote forwarded ports only bind to 127.0.0.1 on the server.

Security Considerations

  • Restrict forwarding in authorized_keys: Limit what a specific key can do:

    no-agent-forwarding,no-X11-forwarding,permitopen="db.internal:5432" ssh-ed25519 AAAA...

    This key can only create tunnels to db.internal:5432.

  • Disable forwarding for specific users: In /etc/ssh/sshd_config:

    Match User restricteduser
        AllowTcpForwarding no
  • TCP-over-TCP performance: SSH tunnels suffer from the TCP-over-TCP problem — the inner and outer TCP stacks both handle retransmissions, which can cause exponential backoff on lossy connections. For high-throughput or latency-sensitive traffic, use WireGuard instead.

  • Audit tunnel usage: Monitor active tunnels on your servers:

    # Show all SSH port forwards in use
    sudo ss -tlnp | grep sshd
    # Show connected SSH sessions with forwarding
    sudo lsof -i -n | grep ssh | grep LISTEN

Summary

  • Local forwarding (-L) brings remote services to your local machine — the most common use case for accessing databases, dashboards, and APIs behind firewalls
  • Remote forwarding (-R) exposes local services on a remote server — useful for development sharing and providing external access to internal tools
  • Dynamic forwarding (-D) creates a SOCKS5 proxy — routes any application’s traffic through the SSH tunnel for secure browsing on untrusted networks
  • Use autossh for persistent tunnels that survive network interruptions, and systemd services for tunnels that must survive reboots
  • SSH config files (~/.ssh/config) eliminate repetitive command-line flags and make tunnels reusable with simple names
  • Add keepalives (ServerAliveInterval 60) to prevent firewalls from dropping idle tunnel connections