SSH Tunneling and Port Forwarding on Linux: Complete Guide with Real-World Use Cases (2026)
π― Key Takeaways
- What Is SSH Tunneling?
- Prerequisites
- Local Port Forwarding (-L)
- Remote Port Forwarding (-R)
- Dynamic Port Forwarding (-D) β SOCKS Proxy
π Table of Contents
- What Is SSH Tunneling?
- Prerequisites
- Local Port Forwarding (-L)
- Remote Port Forwarding (-R)
- Dynamic Port Forwarding (-D) β SOCKS Proxy
- SSH Jump Hosts (ProxyJump)
- SSH Multiplexing β Reuse Connections
- Persistent Tunnels with autossh
- SSH Tunnel Security Best Practices
- Complete ~/.ssh/config Reference
- Troubleshooting Common SSH Tunnel Issues
- Quick Reference β SSH Tunnel Commands
- Conclusion
π Table of Contents
- What Is SSH Tunneling?
- Prerequisites
- Local Port Forwarding (-L)
- Run in Background
- Real Use Cases for Local Forwarding
- Remote Port Forwarding (-R)
- Allow Remote Forwarding from Any Interface
- Real Use Cases for Remote Forwarding
- Dynamic Port Forwarding (-D) β SOCKS Proxy
- Real Use Cases for Dynamic Forwarding
- SSH Jump Hosts (ProxyJump)
- Configure Jump Hosts in ~/.ssh/config
- SSH Multiplexing β Reuse Connections
- Persistent Tunnels with autossh
- Create a systemd Service for Persistent Tunnels
- SSH Tunnel Security Best Practices
- Complete ~/.ssh/config Reference
- Troubleshooting Common SSH Tunnel Issues
- Quick Reference β SSH Tunnel Commands
- Conclusion
SSH tunneling is one of the most powerful and underused capabilities available to Linux sysadmins. It lets you securely forward network traffic through an encrypted SSH connection β bypassing firewalls, accessing services on remote networks, exposing local services to remote machines, and creating encrypted proxies. If you have SSH access to a server, you already have everything you need. This guide covers every type of SSH tunnel with practical real-world examples.
What Is SSH Tunneling?
SSH tunneling (also called SSH port forwarding) wraps network traffic inside an encrypted SSH connection. Instead of connecting directly to a service, your traffic goes through the SSH tunnel, encrypted end-to-end.
# Without tunnel β direct connection (may be blocked or unencrypted)
Your Machine βββββββββββββββββββββββΊ Remote Service
# With SSH tunnel β traffic rides inside SSH
Your Machine ββ[SSH encrypted]βββΊ SSH Server βββΊ Remote Service
There are three types of SSH port forwarding, each solving a different problem:
| Type | Flag | Direction | Use Case |
|---|---|---|---|
| Local | -L | Local port β Remote service | Access remote service locally |
| Remote | -R | Remote port β Local service | Expose local service to remote |
| Dynamic | -D | SOCKS proxy | Route all traffic through server |
Prerequisites
# SSH client (installed by default on Linux/Mac)
ssh -V
# For persistent tunnels, install autossh
# Ubuntu/Debian
sudo apt install autossh
# RHEL/Rocky/AlmaLinux
sudo dnf install autossh
Local Port Forwarding (-L)
Local port forwarding lets you access a remote service as if it were running on your local machine. Traffic goes: your local port β SSH server β target service.
# Syntax
ssh -L [local_port]:[target_host]:[target_port] [ssh_user]@[ssh_server]
# Example: Access a remote MySQL database (port 3306) locally on port 3307
# MySQL is behind a firewall, but your SSH server can reach it
ssh -L 3307:localhost:3306 user@ssh-server.example.com
# Now connect to MySQL on your local machine
mysql -h 127.0.0.1 -P 3307 -u dbuser -p
# Example: Access a web app running on an internal server
# The internal server (192.168.1.50) is only reachable from the SSH server
ssh -L 8080:192.168.1.50:80 user@jump-server.example.com
# Now browse to http://localhost:8080 on your machine β you see the internal web app
Run in Background
# -f = go to background, -N = don't execute remote command, -T = no terminal
ssh -fNT -L 3307:localhost:3306 user@ssh-server.example.com
# Find and kill the tunnel later
ps aux | grep "ssh -fNT"
kill [PID]
Real Use Cases for Local Forwarding
- Database access β Connect to MySQL/PostgreSQL/Redis behind a firewall without a VPN
- Admin panels β Access Grafana, Kibana, or AWX that is bound to localhost on the remote server
- Internal web apps β Browse internal services only accessible from within the network
- RDP through SSH β Tunnel Windows RDP through SSH for encrypted access
# Access Grafana (running on localhost:3000 on your server)
ssh -fNT -L 3000:localhost:3000 user@monitoring-server.example.com
# Now browse http://localhost:3000 on your laptop
# Access a private Kubernetes API server
ssh -fNT -L 6443:10.0.0.1:6443 user@bastion.example.com
kubectl --server=https://localhost:6443 get nodes
# Tunnel RDP to a Windows machine through a Linux jump host
ssh -fNT -L 3389:windows-vm.internal:3389 user@jump.example.com
# Connect RDP client to localhost:3389
Remote Port Forwarding (-R)
Remote port forwarding is the reverse β it exposes a service running on your local machine to the remote SSH server. Traffic goes: remote port β SSH server β your local service.
# Syntax
ssh -R [remote_port]:[local_host]:[local_port] [ssh_user]@[ssh_server]
# Example: Expose your local web server (port 8080) on the remote server's port 80
ssh -R 80:localhost:8080 user@public-server.example.com
# Anyone who connects to public-server.example.com:80 gets your local server
# Example: Expose local development app for a demo
ssh -fNT -R 9000:localhost:3000 user@demo-server.example.com
# Colleagues can access demo-server.example.com:9000 to see your local app
Allow Remote Forwarding from Any Interface
By default, the remote port only binds to localhost on the SSH server. To make it accessible from anywhere, add GatewayPorts yes to the server’s /etc/ssh/sshd_config:
# On the SSH server
echo "GatewayPorts yes" | sudo tee -a /etc/ssh/sshd_config
sudo systemctl reload sshd
# Now specify binding address in the tunnel
ssh -R 0.0.0.0:9000:localhost:3000 user@public-server.example.com
# Port 9000 is now reachable from any IP on public-server
Real Use Cases for Remote Forwarding
- Share local dev environment β Let teammates access your localhost for review without deploying
- Receive webhooks locally β GitHub/Stripe webhooks reach your local development machine
- Remote support β Expose a server behind NAT for support access
- IoT device management β Devices behind NAT call home, you SSH back through the tunnel
Dynamic Port Forwarding (-D) β SOCKS Proxy
Dynamic forwarding turns your SSH connection into a full SOCKS proxy. Instead of forwarding one specific port, ALL traffic from any application is routed through the SSH server.
# Create a SOCKS5 proxy on local port 1080
ssh -fNT -D 1080 user@ssh-server.example.com
# Now configure your browser or application to use SOCKS5 proxy:
# Host: 127.0.0.1 Port: 1080 Type: SOCKS5
# Use with curl
curl --socks5 127.0.0.1:1080 https://api.internal-service.example.com
# Use with git
git config --global http.proxy socks5://127.0.0.1:1080
# Use with Python requests
import requests
proxies = {"http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080"}
response = requests.get("https://internal-api.example.com", proxies=proxies)
Real Use Cases for Dynamic Forwarding
- Route all browser traffic through your server β Access geo-restricted or internal services
- Secure public Wi-Fi β Encrypt all traffic through your SSH server
- Access private network services β Browse internal documentation, APIs, or dashboards
- Bypass corporate firewall β Route application traffic through an allowed SSH connection
SSH Jump Hosts (ProxyJump)
Jump hosts (also called bastion hosts) are intermediate SSH servers you must pass through to reach internal servers. The modern ProxyJump option makes this seamless.
# Old way β two separate SSH commands
ssh user@bastion.example.com
ssh user@internal-server.example.com # run this from inside bastion
# Modern way β single command with ProxyJump
ssh -J user@bastion.example.com user@internal-server.example.com
# Multiple hops β chain jump hosts
ssh -J user@bastion1.example.com,user@bastion2.example.com user@final-server.example.com
Configure Jump Hosts in ~/.ssh/config
nano ~/.ssh/config
# Define the bastion/jump host
Host bastion
HostName bastion.example.com
User admin
IdentityFile ~/.ssh/bastion_key
# Define internal servers that go through the bastion
Host internal-*
User ubuntu
ProxyJump bastion
IdentityFile ~/.ssh/internal_key
Host internal-db
HostName 10.0.1.100
ProxyJump bastion
Host internal-app
HostName 10.0.1.200
ProxyJump bastion
# Now this single command connects through the bastion automatically
ssh internal-db
ssh internal-app
# Copy files through the jump host
scp -J bastion myfile.txt ubuntu@10.0.1.100:/tmp/
# rsync through jump host
rsync -avz -e "ssh -J bastion" ./local-dir/ ubuntu@10.0.1.100:/remote-dir/
SSH Multiplexing β Reuse Connections
SSH multiplexing reuses an existing SSH connection for subsequent connections to the same host β avoiding repeated authentication and speeding up connections significantly.
nano ~/.ssh/config
Host *
# Reuse connections
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600 # keep connection alive for 10 minutes after last use
# Keep connection alive
ServerAliveInterval 60
ServerAliveCountMax 3
# Create socket directory
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
# First connection opens the master socket
ssh user@server.example.com
# Subsequent connections reuse it instantly (no password/key prompt)
ssh user@server.example.com # connects in milliseconds
scp file.txt user@server.example.com:/tmp/ # also uses the socket
Persistent Tunnels with autossh
Regular SSH tunnels die when the connection drops. autossh automatically restarts the tunnel if it fails β essential for production use.
# Install autossh
sudo apt install autossh # Ubuntu/Debian
sudo dnf install autossh # RHEL/Rocky
# Basic autossh usage β restarts tunnel automatically if it fails
autossh -M 20000 -fNT -L 3307:localhost:3306 user@server.example.com
# -M 20000 = monitoring port (autossh sends keepalive on this port)
# Use -M 0 to disable monitoring and rely on ServerAlive settings instead
autossh -M 0 -fNT \
-o "ServerAliveInterval=30" \
-o "ServerAliveCountMax=3" \
-L 3307:localhost:3306 user@server.example.com
Create a systemd Service for Persistent Tunnels
sudo nano /etc/systemd/system/ssh-tunnel-db.service
[Unit]
Description=SSH Tunnel to Database Server
After=network.target
Wants=network-online.target
[Service]
User=ubuntu
ExecStart=/usr/bin/autossh -M 0 -N \
-o "ServerAliveInterval=30" \
-o "ServerAliveCountMax=3" \
-o "ExitOnForwardFailure=yes" \
-i /home/ubuntu/.ssh/tunnel_key \
-L 3307:localhost:3306 \
tunnel-user@db-server.example.com
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-db
# Check status
sudo systemctl status ssh-tunnel-db
journalctl -fu ssh-tunnel-db
SSH Tunnel Security Best Practices
# Create a dedicated user with restricted shell for tunneling only
sudo useradd -r -s /sbin/nologin tunnel-user
# Generate a dedicated key for tunnels
ssh-keygen -t ed25519 -f ~/.ssh/tunnel_key -N "" -C "tunnel-only-key"
# Restrict the key in authorized_keys β allow only port forwarding, no shell
# Edit ~/.ssh/authorized_keys on the server:
no-pty,no-X11-forwarding,no-agent-forwarding,command="/bin/false" ssh-ed25519 AAAA... tunnel-only-key
# Or restrict which ports can be forwarded (in sshd_config)
# PermitOpen localhost:3306
# PermitOpen localhost:5432
# Useful sshd_config options for tunnel security
# /etc/ssh/sshd_config
AllowTcpForwarding local # allow only local forwarding (not remote)
# AllowTcpForwarding yes # allow both directions
# AllowTcpForwarding no # disable all tunneling
GatewayPorts no # don't expose tunnels to network (localhost only)
PermitTunnel no # disable tun/tap tunneling (VPN-style)
Complete ~/.ssh/config Reference
nano ~/.ssh/config
# Global settings
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 10m
Compression yes
AddKeysToAgent yes
# Bastion / Jump host
Host bastion
HostName bastion.company.com
User admin
Port 22
IdentityFile ~/.ssh/company_ed25519
# Internal servers via bastion
Host 10.0.*
User ubuntu
ProxyJump bastion
IdentityFile ~/.ssh/internal_ed25519
StrictHostKeyChecking no
# Database tunnel shortcut
Host db-tunnel
HostName db-server.company.com
User tunnel-user
IdentityFile ~/.ssh/tunnel_key
LocalForward 3307 localhost:3306
LocalForward 6380 localhost:6379
ExitOnForwardFailure yes
ServerAliveInterval 30
# Development server with multiple forwards
Host devbox
HostName dev.company.com
User developer
LocalForward 8080 localhost:8080
LocalForward 3000 localhost:3000
LocalForward 9090 localhost:9090
# Now use it simply
ssh db-tunnel # opens both DB tunnels automatically
ssh devbox # opens all dev port forwards automatically
Troubleshooting Common SSH Tunnel Issues
| Problem | Cause | Fix |
|---|---|---|
| Tunnel dies frequently | Connection timeout | Add ServerAliveInterval=30 to ssh_config |
| Port already in use | Old tunnel still running | kill $(lsof -t -i:LOCAL_PORT) |
| Connection refused on forwarded port | Service not listening, or AllowTcpForwarding=no | Check sshd_config AllowTcpForwarding |
| Remote forwarding not reachable externally | GatewayPorts not set | Add GatewayPorts yes to sshd_config |
| Slow tunnel performance | No compression | Add Compression yes or use -C flag |
| autossh not reconnecting | Wrong monitoring port | Use -M 0 with ServerAlive options |
# Debug SSH tunnel verbosely
ssh -vvv -L 3307:localhost:3306 user@server.example.com
# Check what is listening on forwarded port
ss -tlnp | grep 3307
lsof -i :3307
# Verify tunnel is actually forwarding traffic
curl -v http://localhost:8080 # if forwarding a web service
# Test tunnel connectivity end-to-end
nc -z localhost 3307 && echo "Tunnel working" || echo "Tunnel broken"
Quick Reference β SSH Tunnel Commands
# Local port forwarding
ssh -L LOCAL_PORT:REMOTE_HOST:REMOTE_PORT user@ssh-server
# Local forwarding in background
ssh -fNT -L 3307:localhost:3306 user@server
# Remote port forwarding
ssh -R REMOTE_PORT:LOCAL_HOST:LOCAL_PORT user@ssh-server
# Dynamic SOCKS proxy
ssh -fNT -D 1080 user@ssh-server
# Jump host
ssh -J jumphost user@final-server
# Multiple jump hosts
ssh -J host1,host2 user@final-server
# autossh persistent tunnel
autossh -M 0 -fNT -o "ServerAliveInterval=30" -L 3307:localhost:3306 user@server
# Kill all background tunnels
kill $(pgrep -f "ssh -fNT")
# List active SSH sockets
ls ~/.ssh/sockets/
# Check forwarded ports
ss -tlnp | grep ssh
Conclusion
SSH tunneling is a foundational skill for any Linux sysadmin. Whether you are accessing a database behind a firewall, exposing a local development environment, routing traffic through a secure proxy, or chaining through multiple bastion hosts to reach deeply nested internal servers β SSH tunneling does all of it with encryption baked in, using tools you already have installed. Combined with autossh and systemd for persistence, and a well-organized ~/.ssh/config, SSH tunnels become a reliable, zero-cost alternative to VPNs for many common infrastructure access patterns.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.