SSH Tunneling
SSH tunnels create encrypted channels for forwarding traffic between hosts.
Types of Port Forwarding
Local: Your machine → SSH Server → Target
Remote: Target → SSH Server → Your machine
Dynamic: SOCKS proxy through SSH serverLocal Port Forwarding (-L)
Access remote service through local port.
# Syntax
ssh -L [local_addr:]local_port:remote_host:remote_port user@ssh_server
# Access remote MySQL through local port
ssh -L 3306:localhost:3306 user@server
# Access web app on internal network
ssh -L 8080:internal-server:80 user@bastion
# Bind to all interfaces
ssh -L 0.0.0.0:8080:localhost:80 user@serverUse Case: Access Internal Database
# Forward PostgreSQL
ssh -L 5432:db.internal:5432 user@bastion
# Connect locally
psql -h localhost -p 5432 -U dbuser mydbRemote Port Forwarding (-R)
Expose local service to remote network.
# Syntax
ssh -R [remote_addr:]remote_port:local_host:local_port user@ssh_server
# Expose local web server to remote
ssh -R 8080:localhost:3000 user@server
# Allow external connections
ssh -R 0.0.0.0:8080:localhost:3000 user@serverServer Configuration Required
# /etc/ssh/sshd_config
GatewayPorts yesUse Case: Expose Development Server
# Run local dev server
npm run dev # localhost:3000
# Expose to public server
ssh -R 80:localhost:3000 user@public-server
# Access at http://public-serverDynamic Port Forwarding (-D)
Create a SOCKS proxy.
# Create SOCKS5 proxy on port 1080
ssh -D 1080 user@server
# With background mode
ssh -D 1080 -N -f user@serverConfigure Browser
SOCKS Host: localhost
SOCKS Port: 1080
SOCKS v5: YesUse with curl
curl --socks5 localhost:1080 http://example.comJump Hosts (ProxyJump)
Connect through intermediate hosts.
# Single jump
ssh -J bastion user@internal
# Multiple jumps
ssh -J bastion1,bastion2 user@internalSSH Config
Host internal-server
HostName 10.0.0.50
User admin
ProxyJump bastion.example.comBackground and Persistent Tunnels
Background Mode
# Run in background (-f) without command (-N)
ssh -f -N -L 8080:localhost:80 user@server
# With control socket
ssh -f -N -M -S /tmp/tunnel.sock -L 8080:localhost:80 user@server
# Close tunnel
ssh -S /tmp/tunnel.sock -O exit user@serverAutossh for Persistent Tunnels
# Install autossh
sudo apt install autossh
# Create persistent tunnel
autossh -M 0 -f -N -L 8080:localhost:80 user@server
# With server alive checks
autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -f -N -L 8080:localhost:80 user@serverSystemd Service
# /etc/systemd/system/ssh-tunnel.service
[Unit]
Description=SSH Tunnel to Production
After=network.target
[Service]
User=tunnel
ExecStart=/usr/bin/ssh -N -L 3306:db.internal:3306 user@bastion
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.targetMultiple Tunnels
# Multiple local forwards
ssh -L 3306:db:3306 -L 6379:redis:6379 -L 5432:postgres:5432 user@bastion
# Or in config
Host tunnel-all
HostName bastion.example.com
LocalForward 3306 db:3306
LocalForward 6379 redis:6379
LocalForward 5432 postgres:5432Tunnel + Command
# Run command after tunnel established
ssh -L 5432:db:5432 user@server "psql -h localhost -U user dbname"Troubleshooting
# Verbose output
ssh -v -L 8080:localhost:80 user@server
# Check if port is in use
lsof -i :8080
# Test tunnel
curl localhost:8080Best Practices
- Use ProxyJump - Modern, cleaner syntax
- Use autossh - For persistent tunnels
- Limit forwards - Only what's needed
- Use control sockets - For connection reuse
- Document tunnels - Keep track of what's running
- ssh tunnel
- port forwarding
- socks proxy
- jump host
- bastion