Skip to content
Codeloom
Linux

SSH Tunneling and Port Forwarding Explained

Learn how to use SSH local, remote, and dynamic port forwarding to securely access services across networks.

·7 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • Local, remote, and dynamic SSH port forwarding
  • Practical tunneling scenarios for databases, web apps, and proxies
  • Jump hosts, SSH config, and persistent tunnels

Prerequisites

  • Basic Linux command line
  • Basic SSH connectivity (key-based auth)

What Is SSH Tunneling?

SSH tunneling (also called port forwarding) creates encrypted connections between ports on different machines, routing traffic through an SSH connection. It lets you securely access services that are not directly reachable — like a database behind a firewall or a web interface on a private network.

There are three types of SSH port forwarding:

  1. Local forwarding — Access a remote service as if it were local
  2. Remote forwarding — Expose a local service to a remote network
  3. Dynamic forwarding — Create a SOCKS proxy for all traffic

Local Port Forwarding (-L)

Local forwarding binds a port on your local machine and forwards traffic through SSH to a destination host and port. This is the most common type.

ssh -L [local_addr:]local_port:destination_host:destination_port user@ssh_server

Access a Remote Database

Your PostgreSQL database is on a server that only accepts connections from 10.0.0.0/24. You are on your laptop outside that network, but you have SSH access to a jump box:

# Forward local port 5433 to the database server's port 5432
ssh -L 5433:db.internal:5432 user@jumpbox.example.com

# Now connect to the database as if it's local
psql -h localhost -p 5433 -U myuser mydb

Traffic flows: localhost:5433 -> SSH tunnel -> jumpbox -> db.internal:5432

Access a Remote Web Interface

Many tools (Grafana, Kibana, Jupyter) run on internal servers with web UIs that aren’t publicly accessible:

# Forward Grafana's port 3000
ssh -L 3000:localhost:3000 user@monitoring-server

# Open http://localhost:3000 in your browser

When the destination is localhost, it means localhost from the SSH server’s perspective. The traffic goes through the tunnel to the SSH server, which then connects to its own port 3000.

Multiple Forwards in One Command

ssh -L 5433:db.internal:5432 \
    -L 3000:monitoring.internal:3000 \
    -L 8080:api.internal:8080 \
    user@jumpbox.example.com

Remote Port Forwarding (-R)

Remote forwarding is the reverse: it binds a port on the remote server and forwards traffic back to your local machine. This is useful for exposing a local development server to a remote network.

ssh -R [remote_addr:]remote_port:local_host:local_port user@ssh_server

Expose a Local Dev Server

You are developing a webhook endpoint on your laptop and need an external service to send callbacks to it:

# Make your local port 3000 accessible on the remote server's port 9000
ssh -R 9000:localhost:3000 user@public-server.example.com

# Now anyone who can reach public-server.example.com:9000
# will hit your local development server

For this to work, the SSH server needs GatewayPorts yes in its sshd_config if you want the port accessible from outside the SSH server itself.

Share a Local Service with a Colleague

# Your colleague has SSH access to the same server
ssh -R 8080:localhost:8080 user@shared-server

# Colleague connects to shared-server:8080 to see your app

Dynamic Port Forwarding (-D)

Dynamic forwarding creates a SOCKS proxy on your local machine. Any application that supports SOCKS can route all its traffic through the SSH tunnel. This is like a simple VPN.

ssh -D [local_addr:]local_port user@ssh_server

Create a SOCKS Proxy

# Start a SOCKS5 proxy on local port 1080
ssh -D 1080 user@remote-server.example.com

# Configure your browser to use SOCKS5 proxy at localhost:1080
# All web traffic now goes through the remote server

# Or use curl with the proxy
curl --socks5-hostname localhost:1080 http://internal-app.corp:8080

Browse Internal Networks

If you have SSH access to a corporate network, you can browse all internal services:

ssh -D 9050 user@corp-jumpbox

# Configure Firefox:
# Settings -> Network -> Manual Proxy -> SOCKS Host: localhost, Port: 9050
# Check "Proxy DNS when using SOCKS v5"

Tunnel Flags and Options

Several flags make tunnels more practical:

# -N: Don't execute a remote command (just tunnel)
# -f: Go to background after authentication
# -T: Disable pseudo-terminal allocation

# Background tunnel (no shell, just forwarding)
ssh -fNT -L 5433:db.internal:5432 user@jumpbox

# Check if the tunnel is running
ps aux | grep ssh

# Kill a background tunnel
kill $(pgrep -f "ssh -fNT.*5433")

The combination -fNT is the standard for creating background tunnels that exist purely for port forwarding.

Jump Hosts / ProxyJump (-J)

When you need to SSH through one or more intermediate servers to reach a target:

# Old way: nested SSH
ssh -L 5432:localhost:5432 user@jumpbox ssh -L 5432:db:5432 user@appserver

# Modern way: ProxyJump
ssh -J user@jumpbox user@appserver

# Multiple jumps
ssh -J user@jump1,user@jump2 user@final-destination

# With port forwarding through a jump host
ssh -J user@jumpbox -L 5432:db.internal:5432 user@appserver

SSH Config File

For tunnels you use frequently, configure them in ~/.ssh/config:

# ~/.ssh/config

# Jump host definition
Host jumpbox
    HostName jumpbox.example.com
    User deploy
    IdentityFile ~/.ssh/id_ed25519

# Access internal servers through jump host
Host appserver
    HostName 10.0.1.50
    User deploy
    ProxyJump jumpbox

# Pre-configured tunnel for the database
Host db-tunnel
    HostName jumpbox.example.com
    User deploy
    LocalForward 5433 db.internal:5432
    LocalForward 6380 redis.internal:6379
    RequestTTY no
    ExitOnForwardFailure yes

# SOCKS proxy for internal browsing
Host corp-proxy
    HostName jumpbox.example.com
    User deploy
    DynamicForward 1080
    RequestTTY no

Now you can simply run:

# Connect with all configured forwards
ssh db-tunnel

# Start the SOCKS proxy
ssh -fN corp-proxy

# SSH to appserver through the jump host
ssh appserver

Persistent Tunnels with autossh

SSH tunnels can drop due to network issues. autossh automatically reconnects:

# Install autossh
sudo apt install autossh

# Create a persistent tunnel
autossh -M 0 -fNT \
    -o "ServerAliveInterval 30" \
    -o "ServerAliveCountMax 3" \
    -L 5433:db.internal:5432 \
    user@jumpbox

The -M 0 flag tells autossh to rely on SSH’s built-in keepalive (ServerAliveInterval) instead of its own monitoring port.

For a systemd-managed persistent tunnel:

# /etc/systemd/system/ssh-tunnel-db.service
[Unit]
Description=SSH Tunnel to Database
After=network-online.target
Wants=network-online.target

[Service]
User=deploy
ExecStart=/usr/bin/autossh -M 0 -NT \
    -o "ServerAliveInterval=30" \
    -o "ServerAliveCountMax=3" \
    -o "ExitOnForwardFailure=yes" \
    -L 5433:db.internal:5432 \
    jumpbox
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Security Considerations

# Bind to localhost only (default, secure)
ssh -L 127.0.0.1:5433:db:5432 user@jumpbox

# Bind to all interfaces (allows others on your network to use the tunnel)
ssh -L 0.0.0.0:5433:db:5432 user@jumpbox
# ⚠ Be cautious with this -- anyone on your network can use the forward

# On the server side, restrict forwarding in sshd_config:
# AllowTcpForwarding yes          # allow all forwarding
# AllowTcpForwarding local        # only local forwarding
# AllowTcpForwarding remote       # only remote forwarding
# AllowTcpForwarding no           # disable all forwarding
# PermitOpen db.internal:5432     # restrict which destinations are allowed

Troubleshooting

# Verbose output to debug connection issues
ssh -v -L 5433:db:5432 user@jumpbox

# Check if a local port is already in use
ss -tlnp | grep 5433

# Test if the tunnel is working
nc -zv localhost 5433

# Common errors:
# "bind: Address already in use" -- another process uses that port
# "channel 0: open failed" -- the destination is unreachable from the SSH server
# "Permission denied (publickey)" -- SSH key authentication issue

Summary

SSH tunneling is one of the most practical networking tools available. Local forwarding (-L) lets you access remote services from your machine. Remote forwarding (-R) exposes your local services to a remote network. Dynamic forwarding (-D) creates a SOCKS proxy for routing all traffic. Combine these with ~/.ssh/config for convenience, ProxyJump for multi-hop access, and autossh for persistence. These techniques eliminate the need for VPNs in many scenarios and provide secure, encrypted access to services across network boundaries.