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 -Vto 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:
| Type | Flag | Direction | Use Case |
|---|---|---|---|
| Local forwarding | -L | Local machine → remote destination | Access a remote database, web UI, or service |
| Remote forwarding | -R | Remote server → local machine | Expose local dev server, allow remote access to local services |
| Dynamic forwarding | -D | Local SOCKS proxy → any destination via remote | Secure 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
| Feature | SSH Tunneling | WireGuard VPN | Cloudflare Tunnel | ngrok |
|---|---|---|---|---|
| Encryption | Yes (SSH) | Yes (WireGuard) | Yes (TLS) | Yes (TLS) |
| Requires server software | sshd (ubiquitous) | WireGuard kernel module | cloudflared daemon | ngrok agent |
| Per-port forwarding | Yes | No (full network) | Yes | Yes |
| SOCKS proxy | Yes (-D) | No | No | No |
| Speed overhead | Moderate (TCP-over-TCP) | Low (UDP, kernel-space) | Low | Low |
| Persistent reconnection | With autossh | Built-in | Built-in | Built-in |
| Cost | Free | Free | Free tier available | Free tier available |
| Best for | Quick ad-hoc tunnels, accessing specific services | Site-to-site or full network access | Exposing services to the internet | Quick 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
autosshfor 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