SSH tunnels are one of those tools that seem magical until you understand the three basic patterns. Once you do, you’ll use them constantly.
The Three Types
- Local forwarding (
-L): Access a remote service as if it were local - Remote forwarding (
-R): Expose a local service to a remote network - Dynamic forwarding (
-D): Create a SOCKS proxy through the SSH connection
Let’s break down each one.
Local Forwarding: Reach Remote Services Locally
Scenario: You need to access a database that’s only available from a server you can SSH into.
| |
This creates a tunnel:
- Your
localhost:5432→ throughbastion.example.com→ todatabase.internal:5432
Now connect your database client to localhost:5432:
| |
The database thinks the connection is coming from the bastion, not your laptop.
Syntax Breakdown
| |
local_addr- Optional. Defaults to127.0.0.1. Use0.0.0.0to allow other machines to connect through your tunnel.local_port- Port on your machineremote_host- Target host (from the SSH server’s perspective)remote_port- Target port
Multiple Tunnels
Chain multiple -L flags:
| |
Background Tunnel
Don’t need an interactive shell? Use -f and -N:
| |
-f- Background after authentication-N- Don’t execute remote commands
Find and kill it later:
| |
Remote Forwarding: Expose Local Services
Scenario: You’re developing locally and need to expose your service to a remote server (or the internet).
| |
This creates a tunnel:
remote-server.example.com:8080→ through SSH → to yourlocalhost:3000
Anyone who can reach the remote server on port 8080 now hits your local dev server.
Syntax Breakdown
| |
Common Use Cases
Webhook development: Expose local server for webhook callbacks:
| |
Demo to clients: Share work-in-progress without deploying:
| |
Enabling Remote Binding
By default, remote forwarded ports only bind to 127.0.0.1 on the remote server. To allow external connections, edit /etc/ssh/sshd_config on the remote:
Or use GatewayPorts clientspecified and specify the bind address:
| |
Dynamic Forwarding: SOCKS Proxy
Scenario: You want to route arbitrary traffic through an SSH server.
| |
This creates a SOCKS5 proxy on localhost:1080. Configure your browser or application to use it:
| |
All traffic through the proxy appears to originate from the SSH server.
Use Cases
- Access internal websites from outside the network
- Bypass geographic restrictions by routing through a server in another region
- Secure browsing on untrusted networks (coffee shop WiFi)
Proxy Chains
Route through multiple hops:
| |
SSH Config for Convenience
Codify your tunnels in ~/.ssh/config:
Now just:
| |
Persistent Tunnels with autossh
SSH tunnels die when connections drop. autossh restarts them automatically:
| |
The -M 0 disables autossh’s monitoring port (modern SSH’s ServerAliveInterval is better):
Systemd Service for Boot Persistence
| |
| |
Security Considerations
Limit tunnel permissions - Use
PermitOpeninauthorized_keysto restrict what can be forwarded:Disable forwarding globally if not needed:
Use jump hosts (
-J) instead of agent forwarding for multi-hop:1ssh -J jumpbox.example.com final-destination.example.comAudit tunnel usage - Log forwarded connections:
Quick Reference
| Type | Flag | Direction | Example |
|---|---|---|---|
| Local | -L | Remote → Local | -L 5432:db:5432 |
| Remote | -R | Local → Remote | -R 8080:localhost:3000 |
| Dynamic | -D | SOCKS proxy | -D 1080 |
Mnemonic:
- Local = I want to reach a remote service Locally
- Remote = I want the Remote to reach me
- Dynamic = Don’t know the destination yet (proxy)
SSH tunnels solve real problems without installing additional software. They’re encrypted, authenticated, and work through most firewalls. Once you internalize the three patterns, you’ll wonder how you ever worked without them.