Press ESC to close Press / to search

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

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?

Advertisement
R

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.

🐧 Stay Updated with Linux Tips

Get the latest tutorials, news, and guides delivered to your inbox weekly.

Advertisement

Add Comment


↑