SSH Tunneling and Port Forwarding Explained
Learn how to use SSH local, remote, and dynamic port forwarding to securely access services across networks.
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:
- Local forwarding — Access a remote service as if it were local
- Remote forwarding — Expose a local service to a remote network
- 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.
Related articles
- Linux Linux SSH Port Forwarding Tutorial
Learn local, remote, and dynamic SSH port forwarding with practical examples. Tunnel databases through bastions, expose local apps, and build SOCKS proxies safely.
- Linux iptables and nftables Firewall Configuration on Linux
Learn how to configure Linux firewalls with iptables and nftables, including rules, chains, NAT, and migration strategies.
- Linux Linux File Permissions: A chmod and chown Deep Dive
Understand the Linux permission model from user/group/other to setuid and sticky bits, with practical chmod and chown patterns you can use today.
- Linux Linux iptables Firewall Basics
Learn iptables tables, chains, and rules. Build a working host firewall, understand NAT, and avoid the most common mistakes that lock you out.