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

FlagPurpose
-NDon’t open a shell, just forward ports
-fRun in the background after connecting
-LLocal port forwarding
-RRemote port forwarding
-DDynamic (SOCKS) forwarding
-o ServerAliveInterval=60Keep the connection alive
-o ExitOnForwardFailure=yesExit 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

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.