Three Ways to Tunnel with SSH
When you need to reach a service hidden behind a firewall — a database, an internal web app, a Redis cache — you have a few options: open a firewall port, set up a VPN, or use SSH tunneling. Each has its place. Let me walk through how they compare before getting into the SSH specifics.
Option 1: Open a Firewall Port
The bluntest approach: punch a hole so traffic flows through. It works, but it exposes the service to the open internet. Even with password protection, a publicly accessible database port is an open invitation for brute-force attacks and vulnerability scanners running 24/7.
Option 2: Set Up a VPN
A VPN puts your machine virtually inside the remote network via an encrypted tunnel. Great for teams and corporate environments — but it demands a VPN server, client software, certificate management, and ongoing maintenance. For a solo developer who just needs to poke at a staging database, that’s massive overkill.
Option 3: SSH Tunneling
SSH tunneling forwards a port from one machine to another through an existing SSH connection. No extra software, no certificate authorities, no new firewall rules. If you can SSH into a server, you can tunnel through it.
There are three tunnel types, each solving a different problem:
- Local forwarding — access a remote service as if it were on your local machine
- Remote forwarding — expose a local port to the remote server
- Dynamic forwarding (SOCKS proxy) — route arbitrary traffic through the SSH server
Pros and Cons of Each Approach
Local Port Forwarding
This is the tunnel type you’ll reach for most often. Say your staging server runs PostgreSQL on port 5432, but that port isn’t exposed publicly. Forward it to your local machine like this:
ssh -L 5433:localhost:5432 [email protected]
Point your database client at localhost:5433 and you’re talking to the remote PostgreSQL instance as if it were running locally. Tools like DBeaver, DataGrip, or plain psql work without any special configuration.
- Pro: Dead simple — no config files needed for one-off access
- Pro: Works through most firewalls that allow SSH (port 22)
- Con: The tunnel dies when your SSH session ends
- Con: Forwards one port per command — juggling four services means four terminal tabs
Remote Port Forwarding
Remote forwarding flips the direction. You expose something on your local machine to the remote server. Classic use cases: testing a webhook receiver that needs a public URL, or reaching a dev machine sitting behind NAT that can’t accept inbound connections.
ssh -R 8080:localhost:3000 [email protected]
After this, traffic hitting public-server.com:8080 lands on your local port 3000.
- Pro: Makes local services reachable from the internet without touching your router’s port forwarding
- Con: Requires
GatewayPorts yesin the server’ssshd_configto accept connections from outside localhost - Con: Security risk if not locked down — anyone on the remote server can reach your machine through the tunnel
Dynamic Forwarding (SOCKS Proxy)
Dynamic forwarding turns the SSH connection into a SOCKS5 proxy. Traffic you route through it exits from the SSH server’s network — handy for accessing internal company resources or testing geo-restricted behavior.
ssh -D 1080 [email protected]
- Pro: One command proxies all SOCKS-aware traffic — browser, curl, git, anything that supports a proxy setting
- Pro: Useful for browsing internal resources without a full VPN
- Con: Not all applications support SOCKS5 natively — you may need a wrapper like
proxychains - Con: Requires configuring each application’s proxy settings separately
Recommended Setup
For day-to-day development work, combine two things: SSH config entries for your common tunnels, and the -N flag to run tunnels in the background without opening an interactive shell.
Add Tunnel Definitions to ~/.ssh/config
Stop retyping long commands. Define your tunnels once in ~/.ssh/config:
Host staging-db-tunnel
HostName staging-server.com
User deploy
LocalForward 5433 localhost:5432
ServerAliveInterval 60
ServerAliveCountMax 3
Host prod-redis-tunnel
HostName prod-server.com
User deploy
LocalForward 6380 localhost:6379
ServerAliveInterval 60
ServerAliveCountMax 3
Then bring up either tunnel with a short command:
ssh -N staging-db-tunnel &
ssh -N prod-redis-tunnel &
The -N flag tells SSH not to execute a remote command — it just holds the connection open and keeps the port forwarded. The & backgrounds the process.
ServerAliveInterval and ServerAliveCountMax matter more than most people realize. Without them, idle tunnels drop silently after a few minutes — and you won’t know until a database query times out. With these settings, SSH sends a keepalive every 60 seconds and tolerates 3 missed responses before closing. That’s roughly 3 minutes of resilience through brief network hiccups, which covers most transient outages on cloud infrastructure.
Use autossh for Long-Running Tunnels
Need a tunnel that stays up indefinitely? autossh monitors the connection and restarts it automatically when it drops:
# Install autossh
sudo apt install autossh # Debian/Ubuntu
brew install autossh # macOS
# Start a persistent tunnel
autossh -M 0 -N -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" \
-L 5433:localhost:5432 [email protected]
The -M 0 flag disables autossh’s own monitoring port and relies on SSH’s built-in keepalives instead — the current recommended approach, since the monitoring port approach is fragile and adds unnecessary complexity.
Implementation Guide
Step 1: Basic Local Tunnel
Start simple. Verify your SSH access works, then confirm the tunnel connects before building on top of it:
# Forward remote port 5432 to local port 5433
ssh -L 5433:localhost:5432 -N [email protected]
# In another terminal, test the connection
psql -h localhost -p 5433 -U dbuser mydb
Step 2: Multi-Hop Tunneling
Target service isn’t on the SSH server itself? No problem. If the jump server can reach it, you can reach it too — just specify the internal hostname in the forwarding rule:
# Format: -L local_port:target_host:target_port
ssh -L 5433:db-internal.private:5432 -N [email protected]
db-internal.private is resolved from jump-server.com‘s perspective, not your local machine’s. This lets you reach hosts that don’t even have public DNS entries.
Step 3: Jump Hosts with ProxyJump
OpenSSH 7.3 (released 2016) introduced ProxyJump — a cleaner replacement for the old ProxyCommand netcat pattern that many tutorials still show. Define the chain once in your SSH config:
Host internal-app
HostName 10.0.1.50
User appuser
ProxyJump [email protected]
LocalForward 8080 localhost:8080
Then just run:
ssh -N internal-app
SSH negotiates the two-hop connection automatically. No manual netcat, no shell tricks.
Step 4: Systemd Service for Persistent Tunnels
This setup has been running in production without any babysitting for over a year. Reboots, network drops, SSH disconnects — autossh handles them all. The key is wrapping everything in a systemd service:
# /etc/systemd/system/ssh-tunnel-db.service
[Unit]
Description=SSH Tunnel to Staging DB
After=network.target
[Service]
User=deploy
ExecStart=/usr/bin/autossh -M 0 -N \
-o "ServerAliveInterval=60" \
-o "ServerAliveCountMax=3" \
-o "ExitOnForwardFailure=yes" \
-L 5433:localhost:5432 \
[email protected]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable ssh-tunnel-db
sudo systemctl start ssh-tunnel-db
sudo systemctl status ssh-tunnel-db
Don’t skip ExitOnForwardFailure=yes. Without it, autossh happily stays running while the tunnel silently fails to bind — say, because another process already claimed that local port. You’d see a green service status and zero actual connectivity. This option makes autossh fail loudly so systemd can restart it cleanly.
Quick Security Checklist
- Use SSH key authentication — disable password auth on the server (
PasswordAuthentication noinsshd_config) - Control port forwarding permissions with
AllowTcpForwarding— useMatch Userblocks insshd_configto restrict it per user - For remote forwarding, bind to localhost unless external access is intentional:
-R 127.0.0.1:8080:localhost:3000 - Audit active forwarded ports on the server:
ss -tlnp | grep ssh
SSH tunneling won’t replace a VPN for a 50-person team. But for a solo developer or a small team managing a handful of services, it’s usually all you need. Get the config entries in place once, and accessing a remote database or internal service drops to a single command — encrypted channel included, no extra infrastructure required.

