SSH Port Forwarding and Tunneling: Practical Use Cases

SSH is more than a remote shell. The tunneling capabilities — local forwarding, remote forwarding, and dynamic SOCKS proxying — are genuinely useful for accessing services that aren’t directly reachable, and I reach for them regularly.

Local Port Forwarding

Local forwarding creates a tunnel: you connect to a local port, and SSH forwards the connection to a remote destination.

ssh -L local_port:destination_host:destination_port user@ssh_server

Use case: Access a database on a remote server

The database is on db.internal port 5432, not accessible from the internet. A server gateway.example.com can reach db.internal.

ssh -L 5432:db.internal:5432 user@gateway.example.com

Now localhost:5432 connects through the SSH tunnel to db.internal:5432. Connect with psql:

psql -h localhost -p 5432 -U myuser mydb

Access a service running on the remote server itself:

ssh -L 8080:localhost:8080 user@remote.server.com

Here localhost in the forwarding spec is from the SSH server’s perspective. This forwards your local port 8080 to remote.server.com:8080.

Use case: Access a web UI only listening on localhost

Redis, Prometheus, Grafana, Netdata — many services bind to 127.0.0.1 by default and aren’t exposed to the network. Access them securely:

ssh -L 9090:127.0.0.1:9090 user@monitoring.server.com
# Now open http://localhost:9090 in your browser

Background Tunneling

For long-running tunnels, run in background:

ssh -fN -L 5432:db.internal:5432 user@gateway.example.com

-f — go to background before command execution. -N — don’t execute a remote command (just forward ports).

To close it later, find and kill the SSH process:

ps aux | grep "ssh -fN"
kill <pid>

Or use a more structured approach with control sockets:

# Open tunnel with control socket
ssh -fN -M -S /tmp/ssh-tunnel.sock \
    -L 5432:db.internal:5432 user@gateway.example.com

# Close it cleanly
ssh -S /tmp/ssh-tunnel.sock -O exit user@gateway.example.com

Remote Port Forwarding

Remote forwarding does the reverse: connections to a port on the remote server get forwarded to your local machine.

ssh -R remote_port:local_host:local_port user@remote_server

Use case: Expose a local development server to a client

You’re running a dev server on localhost:3000 and need a client to test it without deploying:

ssh -R 8080:localhost:3000 user@public.server.com

Now public.server.com:8080 forwards to your local localhost:3000. The client connects to the public server’s IP.

For this to work, you need GatewayPorts yes in the SSH server config on public.server.com:

GatewayPorts yes

Without this, the remote port only binds to 127.0.0.1 on the remote server (not accessible externally).

Use case: Access a server behind NAT from outside

Your home server is behind NAT and not directly accessible. Set up a reverse tunnel from the home server to a cloud VPS:

On the home server:

ssh -fN -R 2222:localhost:22 user@cloud.vps.com

Now from anywhere, SSH to the cloud VPS and then jump through:

ssh user@cloud.vps.com
# Then from there:
ssh -p 2222 user@localhost    # This connects back to your home server

Or in one command with ProxyJump:

ssh -J user@cloud.vps.com -p 2222 homeuser@localhost

For persistent reverse tunnels (recovering from network drops), use autossh:

apt install autossh

# Start persistent reverse tunnel
autossh -M 0 -fN -R 2222:localhost:22 \
    -o "ServerAliveInterval=30" \
    -o "ServerAliveCountMax=3" \
    user@cloud.vps.com

autossh monitors the tunnel and restarts it if the connection drops. -M 0 disables autossh’s own monitoring (rely on SSH keepalives instead).

Dynamic SOCKS Proxy

Dynamic forwarding creates a SOCKS5 proxy — your browser or application routes traffic through it, and SSH forwards it from the server.

ssh -D 1080 -fN user@remote.server.com

Configure your browser to use 127.0.0.1:1080 as a SOCKS5 proxy. All traffic routes through the remote server. Useful for:

  • Bypassing geographical restrictions when testing
  • Accessing internal resources that the SSH server can reach
  • Secure browsing on untrusted networks

In Firefox: Preferences → Network Settings → Manual Proxy Configuration → SOCKS Host: 127.0.0.1, Port: 1080, SOCKS5.

Or for specific applications:

# Use SOCKS proxy with curl
curl --socks5 127.0.0.1:1080 http://internal-service.example.com

# Use with git
git config --global http.proxy socks5://127.0.0.1:1080

SSH Config for Common Tunnels

Rather than typing long commands, add tunnels to ~/.ssh/config:

# Jump host configuration
Host bastion
    HostName bastion.example.com
    User ubuntu
    IdentityFile ~/.ssh/bastion_key

# Internal database via tunnel
Host db-tunnel
    HostName bastion.example.com
    User ubuntu
    LocalForward 5432 db.internal:5432
    LocalForward 6379 redis.internal:6379
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

# Access internal web UIs
Host monitoring-tunnel
    HostName monitoring.example.com
    User ubuntu
    LocalForward 9090 localhost:9090
    LocalForward 3000 localhost:3000
    LocalForward 19999 localhost:19999

Then connect with:

ssh -fN db-tunnel
# Now localhost:5432 connects to the database, localhost:6379 to Redis

ProxyJump: Multihop SSH

For SSH through a bastion/jump host:

ssh -J user@bastion.example.com user@internal.server.com

-J specifies a jump host. SSH connects to the bastion first, then tunnels through to the destination.

In ~/.ssh/config:

Host internal-servers
    HostName *.internal
    User ubuntu
    ProxyJump bastion.example.com

Then:

ssh app01.internal    # Automatically routes through bastion

For multiple hops:

ssh -J user@hop1.example.com,user@hop2.example.com user@final.target.com

Security Considerations

Port forwarding is powerful — make sure your SSH server has appropriate restrictions:

# /etc/ssh/sshd_config

# Restrict which users can use tunneling
AllowTcpForwarding yes          # Allow local/remote forwarding
GatewayPorts no                  # Don't allow external access to remote forwardings (default)
X11Forwarding no                 # Disable X11 forwarding unless needed
PermitTunnel no                  # Disable TUN/TAP device tunneling

For service accounts that only need tunneling (not shell access):

# In authorized_keys:
command="",no-pty,no-X11-forwarding,no-agent-forwarding ssh-rsa AAAA...

Or restrict to specific tunnel configurations. SSH’s flexibility here is a double-edged sword — be intentional about what you allow.

For setting up the SSH keys themselves for secure access, the SSH key authentication guide covers key generation, distribution, and troubleshooting. For persistent VPN tunnels that don’t require maintaining an SSH session — useful when you need consistent access to internal networks rather than one-off port forwards — WireGuard VPN offers a simpler and more performant alternative.

Scroll to Top