An SSH tunnel carries other network traffic inside an encrypted SSH connection. It’s the simplest secure way to reach a database or admin interface that isn’t exposed to the internet — no VPN, no firewall changes, nothing to install beyond SSH.

There are three kinds:

Type Flag Direction Typical use
Local -L Your computer → service on/near the server Reach a private database or admin panel
Remote -R Server → service on your computer Show a local dev site to someone; reach a machine behind NAT
Dynamic -D Any destination via the server (SOCKS proxy) Browse through the server’s network/IP

Local forwarding (-L)

Syntax: -L [local_addr:]local_port:destination_host:destination_port

Reach MySQL that listens only on the server’s localhost:

ssh -N -L 3307:127.0.0.1:3306 [email protected]

Connect your client (DBeaver, TablePlus, mysql -h 127.0.0.1 -P 3307) to 127.0.0.1:3307. The destination 127.0.0.1 is resolved on the server, so it means the server’s own localhost.

  • -N — don’t open a shell; just forward.
  • Add -f to send it to the background after authentication.

Reach a host on the server’s private network:

ssh -N -L 8443:10.0.0.20:443 [email protected]

Now https://localhost:8443 on your computer reaches 10.0.0.20:443 via the bastion.

Admin panels on localhost (Portainer, Nginx Proxy Manager, Netdata, phpMyAdmin bound to 127.0.0.1):

ssh -N -L 9443:127.0.0.1:9443 [email protected]

This pattern lets you keep management interfaces completely off the internet.

Remote forwarding (-R)

Syntax: -R [remote_addr:]remote_port:destination_host:destination_port

Make a service on your laptop reachable from the server:

ssh -N -R 8080:127.0.0.1:3000 [email protected]

On the server, curl http://127.0.0.1:8080 reaches port 3000 on your laptop.

By default the remote port binds to the server’s localhost only. To expose it publicly (e.g. to demo a dev site via the server’s IP), set GatewayPorts clientspecified in the server’s sshd config and bind explicitly: -R 0.0.0.0:8080:127.0.0.1:3000. Think twice — you’re publishing your laptop’s service to the internet. For regular use, a reverse proxy or a tool like frp or a Cloudflare Tunnel is more robust.

Reach a machine behind NAT (e.g. a home server): from that machine, keep a reverse tunnel to your VPS:

ssh -N -R 2222:127.0.0.1:22 [email protected]

Then from the VPS: ssh -p 2222 [email protected]. Use autossh or a systemd service to keep it alive.

Dynamic forwarding (-D): a SOCKS proxy

ssh -N -D 1080 [email protected]

Configure your browser (or curl --socks5-hostname 127.0.0.1:1080) to use the SOCKS5 proxy at 127.0.0.1:1080. All traffic goes out from the server’s IP — handy for testing how a site looks from another region, or reaching internal web interfaces by their private names. For everyday privacy, a real VPN such as WireGuard is better.

Save tunnels in ~/.ssh/config

Host db-tunnel
    HostName 203.0.113.10
    User deploy
    LocalForward 3307 127.0.0.1:3306
    LocalForward 9443 127.0.0.1:9443
    ExitOnForwardFailure yes
    ServerAliveInterval 30
ssh -N db-tunnel

ExitOnForwardFailure makes SSH quit if a port can’t be bound (instead of silently connecting without the tunnel). See the SSH config file guide.

Keep tunnels alive

sudo apt install autossh
autossh -M 0 -N -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -L 3307:127.0.0.1:3306 [email protected]

For permanent tunnels, wrap autossh in a systemd service with Restart=always.

Tunnels through a jump host

ssh -N -J alice@bastion -L 5433:127.0.0.1:5432 [email protected]

-J (ProxyJump) hops through the bastion; the forward is set up on the final host.

Security considerations

  • On the server, restrict forwarding where it isn’t needed: AllowTcpForwarding no (globally or per Match User), PermitOpen to limit destinations, GatewayPorts no (default).
  • Keys used only for tunnels can be limited in authorized_keys: permitopen="127.0.0.1:3306",no-pty,command="/bin/false" ssh-ed25519 ….
  • Local forwards bind to 127.0.0.1 by default — keep it that way (-L 0.0.0.0:… would expose the tunnel to your whole local network).

Troubleshooting

  • bind [127.0.0.1]:3307: Address already in use — pick another local port, or stop what’s using it.
  • channel 2: open failed: administratively prohibited — the server disallows forwarding (AllowTcpForwarding).
  • connect failed: Connection refused — the destination isn’t listening where you said (check ss -tlnp on the server).
  • Tunnel drops when idle — add ServerAliveInterval.

Related: MySQL remote access, PostgreSQL remote connections and SSH into your VPS.

  • SSH
  • Networking
  • Security