SSH Tunneling & Port Forwarding: The Complete Guide
Master SSH tunneling with practical examples — local port forwarding, remote port forwarding, dynamic SOCKS proxy, and jump hosts. Access any service securely without a VPN.
Why SSH Tunneling?
You have a database, web panel, or internal service that only listens on localhost or a private network. You need to access it from your laptop — but opening ports to the internet is risky.
SSH tunneling creates an encrypted channel through an existing SSH connection, forwarding traffic between your machine and the remote service. No VPN needed, no firewall changes, no extra software.
Common use cases:
- Access a remote database (MySQL, PostgreSQL, Redis) without exposing it
- Reach an admin panel that only listens on
127.0.0.1 - Browse the internet through a SOCKS proxy on a remote server
- Expose a local development server to the internet
- Chain through jump hosts to reach isolated networks
The Three Types of SSH Tunneling
| Type | Flag | Direction | Use Case |
|------|------|-----------|----------|
| Local forwarding | -L | Remote service to your machine | Access remote DB locally |
| Remote forwarding | -R | Your machine to remote server | Expose local app to internet |
| Dynamic forwarding | -D | SOCKS proxy through remote | Browse via remote server |
Local Port Forwarding (-L)
"Bring a remote service to my machine."
Syntax
ssh -L [local_port]:[target_host]:[target_port] user@ssh_serverExample 1: Access Remote MySQL
MySQL on db-server only listens on 127.0.0.1:3306. You can't connect directly. But you have SSH access to db-server:
ssh -L 3306:127.0.0.1:3306 admin@db-serverNow connect to MySQL as if it were local:
mysql -h 127.0.0.1 -P 3306 -u dbuser -pExample 2: Access a Web Panel
Grafana runs on monitoring-server:3000 but is firewalled:
ssh -L 8080:127.0.0.1:3000 admin@monitoring-serverOpen http://localhost:8080 in your browser — you're viewing Grafana through the SSH tunnel.
Example 3: Access a Service on Another Host
Your SSH server can reach 192.168.1.50:5432 (PostgreSQL on a different machine in the same network):
ssh -L 5432:192.168.1.50:5432 admin@ssh-serverThe tunnel goes: Your laptop → SSH to ssh-server → forwards to 192.168.1.50:5432.
Run in Background
Add -f -N to run the tunnel in the background without opening a shell:
ssh -f -N -L 8080:127.0.0.1:3000 admin@monitoring-server-f— run in background after authentication-N— don't execute any remote command
Remote Port Forwarding (-R)
"Expose my local machine to a remote server."
Syntax
ssh -R [remote_port]:[target_host]:[target_port] user@ssh_serverExample 1: Share Local Dev Server
You're developing a web app on localhost:3000 and want a colleague to see it. You have a public server at public-server.com:
ssh -R 8080:127.0.0.1:3000 admin@public-server.comNow http://public-server.com:8080 shows your local app.
Example 2: Access Home Machine from Work
Your home PC runs an SSH server. From home, create a reverse tunnel to a cloud server:
ssh -R 2222:127.0.0.1:22 admin@cloud-server.comFrom work, connect to your home PC via the cloud server:
ssh -p 2222 homeuser@cloud-server.comEnable Remote Binding
By default, remote forwarded ports only bind to 127.0.0.1 on the server. To make them accessible from other machines, enable GatewayPorts on the SSH server:
# On the SSH server, edit /etc/ssh/sshd_config
GatewayPorts yesOr bind to all interfaces in the command:
ssh -R 0.0.0.0:8080:127.0.0.1:3000 admin@public-server.comDynamic Port Forwarding (-D)
"Use the remote server as a SOCKS proxy."
Syntax
ssh -D [local_port] user@ssh_serverExample: Browse Through a Remote Server
ssh -D 1080 admin@remote-server.comConfigure your browser to use a SOCKS5 proxy at 127.0.0.1:1080. All your web traffic now routes through remote-server.com.
Use with curl
curl --socks5 127.0.0.1:1080 https://ifconfig.me
# Returns the IP of remote-server.com, not yoursFirefox SOCKS Proxy Setup
- Open Firefox Settings
- Search for "proxy"
- Select Manual proxy configuration
- Set SOCKS Host:
127.0.0.1, Port:1080 - Select SOCKS v5
- Check Proxy DNS when using SOCKS v5 (important for privacy)
System-Wide Proxy (Linux)
export ALL_PROXY=socks5://127.0.0.1:1080
export HTTP_PROXY=socks5://127.0.0.1:1080
export HTTPS_PROXY=socks5://127.0.0.1:1080Jump Hosts / ProxyJump (-J)
"SSH through an intermediate server to reach an isolated target."
Your target server (10.0.0.50) is on a private network. You can only reach it through a bastion host (bastion.example.com).
Old Way (ProxyCommand)
ssh -o ProxyCommand="ssh -W %h:%p admin@bastion.example.com" admin@10.0.0.50Modern Way (ProxyJump)
ssh -J admin@bastion.example.com admin@10.0.0.50Multiple Jumps
Chain through multiple hosts:
ssh -J user1@jump1.com,user2@jump2.com admin@final-targetCombine with Tunneling
Forward a port through a jump host:
ssh -J admin@bastion.example.com -L 5432:127.0.0.1:5432 admin@10.0.0.50This creates: Your laptop → bastion → 10.0.0.50 → forwards 5432.
SSH Config File
Typing long SSH commands is tedious. Save them in ~/.ssh/config:
# Jump host / bastion
Host bastion
HostName bastion.example.com
User admin
IdentityFile ~/.ssh/id_ed25519
# Database server (through bastion)
Host db-prod
HostName 10.0.0.50
User admin
ProxyJump bastion
LocalForward 5432 127.0.0.1:5432
# Monitoring server
Host monitoring
HostName monitoring.example.com
User admin
LocalForward 3000 127.0.0.1:3000
LocalForward 9090 127.0.0.1:9090
# SOCKS proxy
Host socks-proxy
HostName remote-server.com
User admin
DynamicForward 1080
# Common settings for all hosts
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
Now access everything with simple commands:
# Tunnel to database through bastion
ssh db-prod
# Open Grafana + Prometheus tunnels
ssh monitoring
# Start SOCKS proxy
ssh -f -N socks-proxySSH Tunneling on Windows
Using OpenSSH (Built into Windows 10/11)
Open PowerShell or Command Prompt — the syntax is identical to Linux:
# Local forwarding
ssh -L 3306:127.0.0.1:3306 admin@db-server
# Dynamic proxy
ssh -D 1080 admin@remote-serverUsing PuTTY
- Open PuTTY, enter the SSH server hostname
- Go to Connection → SSH → Tunnels
- For local forwarding:
- Source port:
8080 - Destination:
127.0.0.1:3000 - Select Local, click Add
- Source port:
- For dynamic forwarding:
- Source port:
1080 - Select Dynamic, click Add
- Source port:
- Go back to Session, save, and connect
Using VS Code
VS Code has built-in port forwarding. When connected to a remote server via SSH:
- Open the Ports panel (bottom bar)
- Click Forward a Port
- Enter the remote port number
- VS Code automatically creates the tunnel
Persistent Tunnels with autossh
SSH tunnels break when the connection drops. Use autossh to auto-reconnect:
# Install
sudo apt install autossh -y
# Create a persistent tunnel
autossh -M 0 -f -N -L 3306:127.0.0.1:3306 admin@db-server \
-o "ServerAliveInterval=30" \
-o "ServerAliveCountMax=3"-M 0— disable autossh's built-in monitoring (use SSH keepalives instead)-o ServerAliveInterval=30— send keepalive every 30 seconds-o ServerAliveCountMax=3— disconnect after 3 missed keepalives
Run as a systemd Service
Create /etc/systemd/system/ssh-tunnel-db.service:
[Unit]
Description=SSH Tunnel to Database
After=network-online.target
Wants=network-online.target
[Service]
User=admin
ExecStart=/usr/bin/autossh -M 0 -N -L 3306:127.0.0.1:3306 admin@db-server -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -i /home/admin/.ssh/id_ed25519
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl enable --now ssh-tunnel-dbThe tunnel now starts on boot and auto-reconnects on failure.
Security Best Practices
1. Use Key-Based Authentication
# Generate a strong key
ssh-keygen -t ed25519 -C "tunnel-key"
# Copy to server
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@server
# Disable password auth on server
# In /etc/ssh/sshd_config:
PasswordAuthentication no2. Restrict Tunnel-Only Users
Create a user that can only tunnel, not get a shell:
# On the server
sudo useradd -m -s /usr/sbin/nologin tunnel-userIn /etc/ssh/sshd_config:
Match User tunnel-user
AllowTcpForwarding yes
X11Forwarding no
PermitTTY no
ForceCommand /bin/false
3. Limit Which Ports Can Be Forwarded
Match User tunnel-user
PermitOpen 127.0.0.1:3306 127.0.0.1:5432
AllowTcpForwarding local
This user can only forward to MySQL and PostgreSQL — nothing else.
4. Use Fail2ban
sudo apt install fail2ban -y
sudo systemctl enable --now fail2banDefault config bans IPs after 5 failed SSH attempts for 10 minutes.
5. Change the SSH Port
# In /etc/ssh/sshd_config
Port 2222Not real security, but eliminates 99% of automated bot traffic.
Quick Reference
Local Forwarding
# Basic
ssh -L 8080:127.0.0.1:80 user@server
# Background
ssh -f -N -L 8080:127.0.0.1:80 user@server
# Multiple ports
ssh -L 3306:127.0.0.1:3306 -L 6379:127.0.0.1:6379 user@serverRemote Forwarding
# Basic
ssh -R 8080:127.0.0.1:3000 user@server
# Bind to all interfaces
ssh -R 0.0.0.0:8080:127.0.0.1:3000 user@serverDynamic Forwarding
# SOCKS proxy
ssh -D 1080 user@server
# Background SOCKS proxy
ssh -f -N -D 1080 user@serverJump Host
# Single jump
ssh -J user@bastion user@target
# Multiple jumps
ssh -J user@jump1,user@jump2 user@target
# With tunnel
ssh -J user@bastion -L 5432:127.0.0.1:5432 user@targetManage Tunnels
# List active SSH connections
ss -tnp | grep ssh
# Kill a background tunnel
kill $(pgrep -f "ssh -f -N -L 8080")
# Kill all SSH tunnels
pkill -f "ssh -f -N"SSH Tunneling vs VPN
| Feature | SSH Tunnel | VPN | |---------|-----------|-----| | Setup time | Seconds (one command) | Minutes to hours | | Scope | Specific ports/services | Entire network | | Encryption | Yes (SSH) | Yes (IPsec/WireGuard) | | Performance | Good for light traffic | Better for heavy traffic | | Requires client software | No (SSH is everywhere) | Yes (VPN client) | | Persistent connections | With autossh | Built-in | | Best for | Quick access to specific services | Full network connectivity |
Use SSH tunneling when you need quick, temporary access to specific services. Use a VPN when you need persistent, full network access for multiple users.