SSH is more than a remote shell. It can create encrypted tunnels that carry
arbitrary TCP traffic between machines — forwarding ports across firewalls,
exposing services on the internet, or routing your entire browser through a
remote server. This chapter covers the three forwarding modes: local, remote,
and dynamic.
What is port forwarding? When SSH forwards a port, it
listens on a port on one machine and transparently delivers traffic to a port
on another machine — through the encrypted SSH connection. The application
sending or receiving traffic doesn't know there's a tunnel involved.
The Three Types at a Glance
Local Forwarding
A port on your machine forwards to a port the server can reach.
ssh -L local_port:target_host:target_port serverUse when: you want to reach a
remote service from your machine — a database, internal website, or
admin panel that isn't exposed publicly.
Remote Forwarding
A port on the server forwards back to a port your machine can
reach.
ssh -R remote_port:target_host:target_port serverUse when: you want the server
(or internet) to reach something on your local machine — exposing a
dev server or a machine behind NAT.
Dynamic (SOCKS Proxy)
A local SOCKS proxy port routes any traffic through the server.
ssh -D local_port serverUse when: you want to browse the
web (or run any app) as if you were on the server's network — bypassing
geo-restrictions or local network filtering.
Local Port Forwarding (-L)
The most common use case: you want to reach something on the remote side
that isn't directly accessible from your machine. Your laptop connects to
localhost:local_port, and SSH silently delivers that traffic
through the tunnel to target_host:target_port — as seen from
the server.
Your machine
localhost8080
➔encrypted SSH tunnel (internet)
SSH Server
server
➔plain connection (local network)
Target
db.internal5432
# Syntax
ssh -L LOCAL_PORT:TARGET_HOST:TARGET_PORTSSH_SERVER# Examples# Access a remote MySQL database on your local port 3307
ssh -L 3307:localhost:3306 server
# Access an internal web service at db.internal:8080 via local port 8080
ssh -L 8080:db.internal:8080 server
# Use the config alias (server must be defined in ~/.ssh/config)
ssh -L 8080:db.internal:8080 server
philip@laptop — tunnel to remote MySQL
# Open the tunnel — SSH gives you a shell AND forwards the portphilip@laptop:~$ssh -L 3307:localhost:3306 serverphilip@server:~$← shell open on server, tunnel active# In a second terminal on your laptop — connect to remote MySQL locallyphilip@laptop:~$mysql -h 127.0.0.1 -P 3307 -u philip -pWelcome to the MySQL monitor. Commands end with ; or \g.← you are connected to the remote MySQL through the tunnel
Run the tunnel in the background (-f -N)
If you don't need an interactive shell — just the tunnel — add -f
(background) and -N (no command / don't open a shell):
philip@laptop — background tunnel, no shell
# -f = fork to background after auth, -N = no shell, just forward the portphilip@laptop:~$ssh -fN -L 3307:localhost:3306 serverphilip@laptop:~$← prompt returns immediately; tunnel running in background# To find and kill the background tunnel laterphilip@laptop:~$ps aux | grep sshphilip 12345 0.0 0.0 ssh -fN -L 3307:localhost:3306 serverphilip@laptop:~$kill 12345
Bind to 0.0.0.0 to share the tunnel on your local network.
By default, the local port listens only on 127.0.0.1 (just your
machine). Use ssh -L 0.0.0.0:8080:target:80 server to let other
devices on your network use the tunnel too.
Remote Port Forwarding (-R)
The mirror image of local forwarding. A port opens on the server
and traffic arriving there is forwarded back through the tunnel to a port your
machine can reach. The canonical use case is exposing a local development server
to the internet without any router configuration.
Internet visitor
anyone
➔hits server port 8080
SSH Server
server8080
➔encrypted SSH tunnel
Your machine
localhost3000
# Syntax
ssh -R REMOTE_PORT:LOCAL_HOST:LOCAL_PORTSSH_SERVER# Expose your local dev server (port 3000) on the server's port 8080
ssh -R 8080:localhost:3000 server
# Anyone hitting http://server:8080 now reaches your local port 3000
philip@laptop — expose local dev server to internet
# Your React/Node/Flask app is running on port 3000philip@laptop:~$npm start Local: http://localhost:3000# In another terminal — open reverse tunnelphilip@laptop:~$ssh -R 8080:localhost:3000 serverphilip@server:~$# Your client can now open http://server.example.com:8080 and see your app# No router changes, no port opening, no DynDNS — just SSH
Allow external connections to the remote port
By default, the remote port binds only to 127.0.0.1 on the
server — reachable from the server itself but not from outside. To allow
external access, set GatewayPorts yes in the server's
/etc/ssh/sshd_config, or specify the bind address explicitly:
# Bind to all interfaces on the server — anyone can reach it
ssh -R 0.0.0.0:8080:localhost:3000 server
# On the server — enable GatewayPorts in sshd_config if needed# /etc/ssh/sshd_config
GatewayPorts yes
# Then: sudo systemctl restart sshd
Remote forwarding opens your local machine to the internet
— only do it on a server you trust and control. Anyone who can reach the
server's open port can connect to your local service. Always restrict access
with firewall rules if exposing sensitive services.
Dynamic Port Forwarding — SOCKS Proxy (-D)
Instead of forwarding one specific port to one specific destination, dynamic
forwarding creates a SOCKS5 proxy on a local port. Any SOCKS-aware application
(browser, curl, git) can be pointed at it, and all its traffic will travel through
the SSH connection — leaving the server and appearing to originate from the
server's IP address.
Your browser
SOCKS51080
➔encrypted SSH tunnel
SSH Server
server your proxy exit
➔to any URL via server
Destination
any website
# Open a SOCKS5 proxy on local port 1080
ssh -D 1080 server
# Or background it so you keep your terminal
ssh -fN -D 1080 server
Then point your browser's proxy settings at SOCKS5 127.0.0.1:1080:
philip@laptop — SOCKS proxy via curl and Firefox
# Start the SOCKS proxy in the backgroundphilip@laptop:~$ssh -fN -D 1080 server# Use it with curl — your traffic exits from server's IPphilip@laptop:~$curl --socks5 127.0.0.1:1080 https://ifconfig.me203.0.113.42← server's IP address, not your laptop's# Use it with git — route all git traffic through the tunnelphilip@laptop:~$git config --global http.proxy socks5://127.0.0.1:1080# In Firefox: Settings → Network → Manual Proxy → SOCKS5 Host: 127.0.0.1 Port: 1080# Enable "Proxy DNS when using SOCKS v5" to also tunnel DNS queries
Practical Use Cases
Access a remote database securely
MySQL/Postgres only listens on 127.0.0.1 on the server (correct). Tunnel it to your laptop so your GUI client (TablePlus, DBeaver) can connect.
ssh -fN -L 3307:localhost:3306 server
# Connect TablePlus to 127.0.0.1:3307
Browse an internal admin panel
Grafana / phpMyAdmin / Proxmox web UI is on an internal host not exposed to the internet.
ssh -fN -L 8080:grafana.internal:3000 server
# Open http://localhost:8080 in your browser
Show a client your local dev site
Your Node app is running on localhost:3000. Client wants to see it now, no time to deploy.
ssh -R 8080:localhost:3000 server
# Client opens http://server.example.com:8080
SSH into a machine behind NAT
Your home Raspberry Pi is behind a home router. You want SSH access from work without port forwarding the router.
# Run on the Pi at home:
ssh -fN -R 2222:localhost:22 server
# From work:
ssh -p 2222 pi@server
Browse the web via server's network
You're on a restrictive hotel/coffee-shop Wi-Fi and want to route traffic through your VPS.
ssh -fN -D 1080 server
# Set browser SOCKS5 proxy: 127.0.0.1:1080
Access entire remote subnet
Multiple services on a private network (192.168.10.x). Forward a different local port for each, or use SOCKS and configure the browser.
ssh -fN -D 1080 server
# Point browser proxy to 127.0.0.1:1080
# All 192.168.10.x addresses now reachable
Port Forwarding in the SSH Config File
Frequently used tunnels belong in ~/.ssh/config — then you
don't need to remember the flags, and the tunnel opens automatically when you
connect to that host:
# ~/.ssh/config — permanent local tunnel for the databaseHostserverHostName192.168.1.100UserphilipIdentityFile~/.ssh/id_ed25519LocalForward3307 localhost:3306← always tunnel MySQLLocalForward8080 grafana.internal:3000← and Grafana# Now ssh server automatically sets up both tunnels
# config directives for each forwarding typeLocalForwardlocal_port target_host:target_port← -L equivalentRemoteForwardremote_port local_host:local_port← -R equivalentDynamicForwardlocal_port← -D equivalent
Quick Reference
Flag / directive
What it does
-L local:host:remote
Local forward — local port → remote target (via server)
-R remote:host:local
Remote forward — server port → your local target
-D port
Dynamic SOCKS5 proxy on local port
-N
No shell — just set up the tunnel, no interactive session
-f
Fork to background after authenticating
-fN
Background tunnel with no shell (use together for clean background tunnels)
LocalForward
Put -L equivalent in ~/.ssh/config
RemoteForward
Put -R equivalent in ~/.ssh/config
DynamicForward
Put -D equivalent in ~/.ssh/config
GatewayPorts yes
Allow external connections to remote-forwarded ports (in sshd_config)
curl --socks5 127.0.0.1:PORT url
Use a SOCKS proxy with curl
ps aux | grep ssh
Find background SSH tunnel processes to kill them
Next — Chapter 6: SFTP.
SSH can transfer files as well as open shells. SFTP (SSH File Transfer Protocol)
runs over the same connection and gives you an interactive file browser —
upload, download, rename, delete, and navigate directories on the remote machine.
Chapter 6 covers the sftp command, its most useful subcommands,
batch transfers, and GUI clients like FileZilla and WinSCP.