SSH Tunneling Guide for Self-Hosting
What Is SSH Tunneling?
SSH tunneling (port forwarding) encrypts and forwards network traffic through an SSH connection. It lets you securely access services on a remote server as if they were running locally — without opening additional ports or setting up a VPN.
For self-hosting, SSH tunnels are useful when:
- You need to access a web UI (Portainer, Grafana, database admin) that isn’t exposed through your reverse proxy
- You’re debugging a service and need temporary access to an internal port
- You want to access your server securely from a coffee shop without a full VPN
- You need to bypass restrictive firewalls
SSH tunneling requires SSH access to your server — see SSH Setup if you haven’t configured that yet.
Prerequisites
- SSH access to your server with key-based authentication — see SSH Setup
- OpenSSH client on your local machine (built into Linux, macOS, and Windows 10+)
- A service running on your server that you want to access
Local Port Forwarding
Local forwarding maps a port on your local machine to a port on the remote server. Traffic to localhost:LOCAL_PORT gets tunneled through SSH to REMOTE_HOST:REMOTE_PORT.
Syntax
ssh -L LOCAL_PORT:REMOTE_HOST:REMOTE_PORT user@server
Example: Access Portainer
Portainer runs on port 9443 on your server but isn’t exposed through your reverse proxy. Forward it to your local machine:
ssh -L 9443:localhost:9443 [email protected]
Now open https://localhost:9443 in your browser. The traffic is encrypted through the SSH tunnel to Portainer on the server.
Example: Access a Database
PostgreSQL runs on port 5432 on your server, only listening on localhost (not exposed to the network). Connect to it from your local machine:
ssh -L 5432:localhost:5432 [email protected]
Now your local database tools (pgAdmin, DBeaver, psql) can connect to localhost:5432 and reach the remote database.
If you already have PostgreSQL running locally on 5432, use a different local port:
ssh -L 15432:localhost:5432 [email protected]
Connect your tools to localhost:15432 instead.
Example: Access a Service on a Different Host
SSH tunnels can forward to any host reachable from the SSH server, not just localhost. If your server can reach a NAS at 192.168.1.20:
ssh -L 5000:192.168.1.20:5000 [email protected]
This forwards localhost:5000 through the SSH server to port 5000 on the NAS. Useful when the NAS isn’t directly reachable from your current network.
Remote Port Forwarding
Remote forwarding is the reverse — it makes a port on your local machine accessible from the remote server. Traffic to SERVER:REMOTE_PORT gets tunneled back to LOCAL_HOST:LOCAL_PORT.
Syntax
ssh -R REMOTE_PORT:LOCAL_HOST:LOCAL_PORT user@server
Example: Expose a Local Development Server
You’re developing a web app locally on port 3000 and want to test it from your server or share it temporarily:
ssh -R 8080:localhost:3000 [email protected]
On the server, localhost:8080 now reaches your local development server. By default, remote forwarding only binds to the server’s localhost. To make it accessible on the server’s network, the SSH server must have GatewayPorts yes in /etc/ssh/sshd_config.
Example: Expose a Service Behind CGNAT
If your home server is behind CGNAT (no public IP), you can expose a service through a VPS:
# On your home server, create a tunnel to your VPS
ssh -R 8080:localhost:8096 [email protected]
Now your-vps.com:8080 reaches Jellyfin on your home server. For a more robust version of this, consider Cloudflare Tunnel or Tailscale.
Dynamic Port Forwarding (SOCKS Proxy)
Dynamic forwarding creates a SOCKS proxy on your local machine. All traffic routed through the proxy gets tunneled through the SSH server. This is useful for routing browser traffic through your server.
Syntax
ssh -D LOCAL_PORT user@server
Example: Browse Through Your Server
ssh -D 1080 [email protected]
Configure your browser to use localhost:1080 as a SOCKS5 proxy. All browser traffic now goes through your server’s network. This lets you access internal services by their local IPs and hostnames.
In Firefox: Settings → Network Settings → Manual proxy → SOCKS Host: localhost, Port: 1080, SOCKS v5.
Useful Flags
| Flag | Purpose |
|---|---|
-N | Don’t open a shell, just forward ports |
-f | Run in the background after connecting |
-L | Local port forwarding |
-R | Remote port forwarding |
-D | Dynamic (SOCKS) forwarding |
-o ServerAliveInterval=60 | Keep the connection alive |
-o ExitOnForwardFailure=yes | Exit if port forwarding fails |
Combining Flags
Run a tunnel in the background without a shell:
ssh -fN -L 9443:localhost:9443 [email protected]
This starts the tunnel and returns you to your terminal. The SSH process runs in the background.
To stop a background tunnel:
# Find the SSH process
ps aux | grep "ssh -fN"
# Kill it
kill <PID>
Persistent Tunnels with autossh
Plain SSH tunnels break when the network drops. autossh monitors the connection and automatically reconnects.
# Install autossh
sudo apt install -y autossh
# Start a persistent tunnel
autossh -M 0 -fN -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" \
-L 9443:localhost:9443 [email protected]
The -M 0 flag uses SSH’s built-in keepalive (ServerAliveInterval) instead of autossh’s monitoring port. ServerAliveCountMax=3 means disconnect after 3 missed keepalives (90 seconds).
autossh as a systemd Service
For tunnels that should survive reboots:
# /etc/systemd/system/ssh-tunnel-portainer.service
[Unit]
Description=SSH Tunnel to Portainer
After=network-online.target
Wants=network-online.target
[Service]
User=tunneluser
ExecStart=/usr/bin/autossh -M 0 -N -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -L 9443:localhost:9443 [email protected]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable --now ssh-tunnel-portainer
SSH Config for Easier Tunneling
Add tunnel configurations to ~/.ssh/config to avoid typing long commands:
# ~/.ssh/config
Host server-portainer
HostName yourserver.com
User myuser
LocalForward 9443 localhost:9443
IdentityFile ~/.ssh/id_ed25519
Host server-db
HostName yourserver.com
User myuser
LocalForward 5432 localhost:5432
LocalForward 6379 localhost:6379
IdentityFile ~/.ssh/id_ed25519
Host server-proxy
HostName yourserver.com
User myuser
DynamicForward 1080
IdentityFile ~/.ssh/id_ed25519
Now connect with just:
ssh -fN server-portainer
Common Mistakes
Forgetting -N (Opening an Interactive Shell)
Without -N, SSH opens a shell session alongside the tunnel. If you only need the tunnel, add -N to avoid an unnecessary shell.
Port Already in Use
If the local port is already in use, the tunnel silently fails to bind. Check with:
ss -tlnp | grep :9443
Use a different local port if needed: -L 19443:localhost:9443.
Tunnel Drops Without Keepalive
Network interruptions or idle timeouts kill SSH connections. Always use ServerAliveInterval:
ssh -o ServerAliveInterval=60 -L 9443:localhost:9443 user@server
Or set it globally in ~/.ssh/config:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Using SSH Tunnels for Everything
SSH tunnels are great for ad-hoc access and debugging. For permanent remote access to multiple services, use a proper solution: Tailscale, WireGuard, or a reverse proxy with Cloudflare Tunnel.
Next Steps
- Set up SSH properly — SSH Setup
- For permanent remote access — Tailscale Setup or WireGuard VPN Setup
- Secure your server — Firewall Setup with UFW
- Learn about reverse proxies — Reverse Proxy Explained
FAQ
Is SSH tunneling secure?
Yes. Traffic through an SSH tunnel is encrypted end-to-end with the same algorithms used for your SSH session. It’s as secure as your SSH connection — use key-based authentication and disable password auth for best security.
Can I forward multiple ports at once?
Yes. Add multiple -L flags:
ssh -L 9443:localhost:9443 -L 3000:localhost:3000 -L 5432:localhost:5432 user@server
Or define multiple LocalForward lines in ~/.ssh/config.
SSH tunnel vs VPN — when to use which?
SSH tunnels are per-port and ad-hoc — good for accessing specific services temporarily. A VPN (Tailscale, WireGuard) gives your device full network access to the remote network — better for permanent access to multiple services. Use SSH tunnels for quick access; use a VPN for daily use.
Related
Get self-hosting tips in your inbox
New guides, comparisons, and setup tutorials — delivered weekly. No spam.