Linux SSH Tunneling: Port Forwarding
SSH tunneling leverages the SSH protocol to create encrypted channels for arbitrary TCP traffic. While SSH is primarily known for remote shell access, its port forwarding capabilities turn it into a...
Key Insights
- SSH tunneling transforms your SSH connection into a secure conduit for routing traffic, enabling you to access remote services, bypass restrictive firewalls, and expose local services without VPN complexity.
- Local port forwarding (
-L) pulls remote services to your machine, remote port forwarding (-R) pushes local services to remote networks, and dynamic forwarding (-D) creates a SOCKS proxy for flexible routing. - Production tunnels require proper configuration management through SSH config files, persistent connection handling with autossh, and security hardening to prevent unauthorized port forwarding abuse.
Introduction to SSH Tunneling
SSH tunneling leverages the SSH protocol to create encrypted channels for arbitrary TCP traffic. While SSH is primarily known for remote shell access, its port forwarding capabilities turn it into a versatile networking tool that solves real-world problems: accessing databases behind firewalls, securing unencrypted protocols, and exposing development servers for testing.
Three types of SSH tunneling exist, each serving distinct use cases. Local port forwarding brings remote services to your local machine. Remote port forwarding exposes your local services to remote networks. Dynamic port forwarding creates a SOCKS proxy that routes multiple connections through the SSH tunnel. Understanding when to use each type is essential for effective SSH tunnel management.
Local Port Forwarding
Local port forwarding maps a port on your local machine to a destination accessible from the SSH server. The SSH server acts as an intermediary, forwarding your traffic to the final destination. This is invaluable when you need to access services that aren’t directly reachable from your network.
The basic syntax follows this pattern:
ssh -L [bind_address:]local_port:destination:destination_port user@ssh_server
A common scenario involves accessing a remote database that only accepts connections from localhost for security. Instead of exposing the database to the internet, you create a tunnel:
ssh -L 3306:localhost:3306 user@db-server.example.com
This command forwards your local port 3306 to port 3306 on the remote server. Your database client connects to localhost:3306, and the traffic securely tunnels through SSH to the remote database. The database sees the connection as originating from localhost, satisfying its security restrictions.
You can forward to any destination reachable from the SSH server, not just localhost:
ssh -L 5432:internal-db.corp.local:5432 user@jumphost.example.com
This accesses an internal database server through a jump host. The jump host forwards your traffic to internal-db.corp.local:5432, enabling access to internal resources without VPN.
Multiple port forwards work in a single SSH command:
ssh -L 3306:db-server:3306 -L 6379:redis-server:6379 -L 9200:elasticsearch:9200 user@jumphost
This establishes three simultaneous tunnels, forwarding MySQL, Redis, and Elasticsearch ports through one SSH connection.
By default, forwarded ports only bind to localhost. To allow other machines on your network to use the tunnel, specify a bind address:
ssh -L 0.0.0.0:3306:localhost:3306 user@db-server
This binds to all interfaces, but use it cautiously—it exposes the tunnel to your entire network.
Remote Port Forwarding
Remote port forwarding works in reverse: it exposes a service running on your local machine (or accessible from it) to the remote SSH server’s network. This is perfect for demonstrating local development work to remote colleagues or providing temporary access to services running on your machine.
The syntax mirrors local forwarding but uses -R:
ssh -R [bind_address:]remote_port:destination:destination_port user@ssh_server
A practical example involves exposing your local web development server:
ssh -R 8080:localhost:3000 user@remote-server.example.com
Your application runs on localhost:3000. The remote server now has port 8080 listening, forwarding connections back through the SSH tunnel to your local port 3000. Anyone who can reach the remote server at remote-server.example.com:8080 accesses your local application.
This technique enables webhook testing during development. Many third-party services require publicly accessible URLs for webhooks. Instead of deploying to a staging environment, you can temporarily expose your local server:
ssh -R 9000:localhost:4000 user@public-server.com
Configure the webhook to hit http://public-server.com:9000, and requests route to your local development environment.
Remote port forwarding requires the SSH server to permit it. Check the remote server’s /etc/ssh/sshd_config for:
AllowTcpForwarding yes
GatewayPorts yes # Required for binding to non-localhost addresses
Without GatewayPorts, remote forwards only bind to localhost on the remote server, limiting accessibility.
Dynamic Port Forwarding (SOCKS Proxy)
Dynamic port forwarding creates a SOCKS proxy server on your local machine, routing all traffic through the SSH connection. Unlike local and remote forwarding, which handle specific ports, dynamic forwarding handles arbitrary connections, making it ideal for routing multiple applications through the tunnel.
Create a SOCKS proxy with:
ssh -D 1080 user@ssh-server.example.com
This establishes a SOCKS5 proxy listening on localhost:1080. Configure applications to use this proxy, and their traffic routes through the SSH tunnel.
Configure Firefox to use the SOCKS proxy by setting network settings to manual proxy configuration with SOCKS Host: localhost, Port: 1080, and selecting SOCKS v5.
For command-line tools, many support SOCKS proxies directly:
curl --socks5 localhost:1080 http://internal-api.corp.local/status
This routes the curl request through your SSH tunnel, enabling access to internal resources.
Git also supports SOCKS proxies for specific repositories:
git config --global http.proxy 'socks5://localhost:1080'
Dynamic forwarding excels when you need flexible routing without pre-defining specific ports. It’s particularly useful for accessing multiple services behind restrictive firewalls or routing all traffic from a specific application through the tunnel for privacy.
Advanced Techniques and Options
Production SSH tunnels require additional flags for reliability and usability.
The -N flag tells SSH not to execute remote commands, useful for dedicated tunnel connections:
ssh -N -L 3306:localhost:3306 user@db-server
Combine with -f to background the process:
ssh -fNL 3306:localhost:3306 user@db-server
This creates the tunnel and immediately returns control to your shell.
The -g flag allows remote hosts to connect to local forwarded ports, similar to binding to 0.0.0.0:
ssh -g -L 8080:localhost:80 user@server
For persistent tunnels that survive network interruptions, use autossh:
autossh -M 0 -fNL 3306:localhost:3306 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" user@db-server
The -M 0 disables autossh’s built-in monitoring port (using SSH’s built-in keepalive instead). ServerAliveInterval sends keepalive packets every 30 seconds, and ServerAliveCountMax terminates after 3 failed attempts, triggering autossh to reconnect.
Install autossh on Debian/Ubuntu:
sudo apt install autossh
Or on RHEL/CentOS:
sudo yum install autossh
Security Considerations and Best Practices
SSH tunneling’s power demands security discipline. Unrestricted port forwarding can expose internal services to unauthorized users.
Use SSH config files (~/.ssh/config) to manage tunnel configurations:
Host db-tunnel
HostName db-server.example.com
User dbadmin
LocalForward 3306 localhost:3306
ServerAliveInterval 60
ServerAliveCountMax 3
Connect with simply:
ssh db-tunnel
Always use key-based authentication for automated tunnels:
ssh-keygen -t ed25519 -C "tunnel-key"
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
Restrict port forwarding in /etc/ssh/sshd_config on servers:
AllowTcpForwarding local # Allows local forwarding only
# Or
AllowTcpForwarding no # Disables all forwarding
For user-specific restrictions, use Match blocks:
Match User restricted-user
AllowTcpForwarding no
Monitor active tunnels regularly:
ps aux | grep ssh | grep -E '\-[LRD]'
Check listening ports:
netstat -tlnp | grep ssh
# Or on modern systems
ss -tlnp | grep ssh
List established SSH connections:
lsof -i -n | grep ssh
Troubleshooting Common Issues
When tunnels fail, systematic debugging reveals the cause.
Enable verbose output to see connection details:
ssh -vvv -L 8080:localhost:80 user@server
The output shows authentication steps, forwarding setup, and connection errors.
“Port already in use” errors occur when the local port is occupied:
lsof -i :3306
Kill the conflicting process or choose a different local port.
“Permission denied” for ports below 1024 requires root privileges:
sudo ssh -L 80:localhost:8080 user@server
Better practice: use ports above 1024 and configure local port forwarding with iptables if you need privileged ports.
Connection timeouts often indicate firewall blocking. Verify the SSH server allows the connection:
telnet ssh-server.example.com 22
Test port availability after establishing a tunnel:
nc -zv localhost 8080
If the tunnel establishes but traffic doesn’t flow, verify the destination is reachable from the SSH server:
ssh user@server 'nc -zv destination-host 3306'
For tunnels that disconnect frequently, adjust keepalive settings:
ssh -L 3306:localhost:3306 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" user@server
This sends keepalive packets every 30 seconds, maintaining the connection through NAT timeouts and idle connection terminators.
SSH tunneling transforms secure shell access into a comprehensive networking tool. Master these techniques, and you’ll navigate complex network topologies, access restricted services, and secure unencrypted protocols without deploying heavyweight VPN infrastructure. The key is understanding which forwarding type solves your specific problem and implementing proper security controls to prevent abuse.