Blog

July 2, 2026

A Practical Guide on SSH Port Forwarding

A practical guide to SSH port forwarding, including local, remote and automation.

By Julian

SSH port forwarding lets you publish or consume services over an SSH connection.

This is a short guide to the most common use cases, with examples.

TL;DR

If you just need the shortcuts, use these:

Local forward: -L

Reach a private service

Open a port on your machine and forward it to a host and port reachable from the SSH server.

Hover for example
ssh -L <localport>:<target-host>:<target-port> <user>@<ssh-host>

Remote forward: -R

Share a local app remotely

Open a port on the remote side and send traffic back to your machine.

Hover for example
ssh -R <remoteport>:<local-host>:<local-port> <user>@<ssh-host>

Useful extras

Add `-N` to not start a shell, add `-f` to send SSH into the background, add `-o ExitOnForwardFailure=yes` to fail fast, and use `autossh` with SSH keepalives to auto-reconnect.

Hover for example
ssh -N -f -L <localport>:<host>:<hostport> <ssh-host>

Remote port forwarding: -R

Goal: let a remote machine reach something running on your machine.

What you can do: run a dev server on your laptop and let a remote colleague access it, or receive webhooks from a service that cannot reach your laptop directly.

What it does: open a port on the remote machine and forward that traffic back to a host and port on your machine.

🧔 user -> ☁️ internet / network where the server is → 🖥 gateway.example.com:8080 → 🔐 SSH tunnel → 💻 localhost:3000 (your service running locally)

ssh -R 8080:localhost:3000 user@gateway.example.com

Now any process on gateway.example.com that can reach localhost:8080 can connect to it, and that traffic is sent to localhost:3000 on your machine.

Pro tip

You can choose on which interface (on your server) the port should be exposed.
By default, remote forwards are usually only reachable from the server itself. If you want the client to request a public bind address such as 0.0.0.0, the SSH server must allow it with GatewayPorts clientspecified (or GatewayPorts yes, which forces remote forwards onto the wildcard address).

Here is an example of how to make the port reachable from any machine on the server's network (and internet if the server is reachable from the internet):

ssh -R 0.0.0.0:8080:localhost:3000 user@gateway.example.com

Setting GatewayPorts

GatewayPorts is configured on the SSH server, not on your laptop. Edit the server's sshd_config:

# /etc/ssh/sshd_config
GatewayPorts clientspecified

Then reload SSH:

sudo systemctl reload sshd

Use clientspecified when you want the ssh -R command to decide the bind address, for example 127.0.0.1, 0.0.0.0, or a specific server IP. Use yes only if you want remote forwards to bind to the wildcard address by default. The safer default is no, which keeps remote forwards reachable only from the SSH server itself.

If this is a shared server, also consider restricting what users can expose with PermitListen, for example:

PermitListen 127.0.0.1:8080

Local port forwarding: -L

Goal: reach a service on the remote side from your machine.

What you can do: access a private service such as a database that is only reachable from the remote machine or its network.

What it does: open a port on your machine and forward that traffic through SSH to a host and port reachable from the SSH server.

💻 localhost:5432 → 🔐 SSH tunnelgateway.example.com → 🗄️ localhost:5432

ssh -L 5432:localhost:5432 user@gateway.example.com

Pro tip

You can forward to a different host than the SSH server itself. For example, if the SSH server can reach a database on the internal network, you can forward to that database instead of the SSH server:

ssh -L 5432:database-server:5432 user@gateway.example.com

Important: with -L, the destination hostname (e.g. database-server) is resolved from the SSH server's point of view. With -R, the destination hostname is resolved from your machine's point of view. This is a common source of confusion — if a hostname works in one direction but not the other, check which side is doing the DNS lookup.

Real-world example

Let's say you have a local ollama instance running on your machine, and you want to share this with a coworker. You can publish the local port 11434 to the remote server using a remote forward:

ssh -R 11434:localhost:11434 flotte.sh

Now your coworker can forward the remote port 11434 to their local machine and access your ollama instance:

ssh -L 11434:localhost:11434 flotte.sh

And then your coworker can access your ollama instance at localhost:11434 on their machine.

🧔 Co-Worker → 🔐 SSH tunnel -L → 🖥 flotte.sh:11434 (bound to localhost) → 🔐 SSH tunnel → 💻 localhost:11434 -R (your ollama instance)

Or you can just publish ollama to the internet using a remote forward and a public server:

ssh -R 0.0.0.0:11434:localhost:11434 flotte.sh

⚠️ Security note: if the SSH server allows a public remote bind, binding to 0.0.0.0 can make the port reachable from the internet when the server and firewall permit it. Only do this with services that have authentication in place, or restrict access with a firewall.

Practical tips

Useful flags:

  • -N: create the tunnel without opening a remote shell
  • -f: move SSH to the background after it connects
  • ExitOnForwardFailure=yes: fail if the tunnel could not actually be created
  • ServerAliveInterval and ServerAliveCountMax: detect dead connections sooner
  • autossh: restart the tunnel if it drops

If you only want the tunnel, add -N:

ssh -N -L 5432:db.internal:5432 \
    -o ExitOnForwardFailure=yes \
    julian@bastion.example.com
ssh -N -L 5432:db.internal:5432 \
    -o ServerAliveInterval=30 \
    -o ServerAliveCountMax=3 \
    julian@bastion.example.com

Autossh

If you want to keep the tunnel alive even if the connection drops, use autossh with SSH keepalives:

autossh -M 0 -N \
    -o ServerAliveInterval=30 \
    -o ServerAliveCountMax=3 \
    -L 5432:db.internal:5432 \
    julian@bastion.example.com

The -M 0 flag disables autossh's built-in monitoring port. The SSH keepalive options make the SSH client exit when the server stops responding, which gives autossh a failure to restart. This avoids consuming an extra monitoring port.

Put repeatable tunnels in ~/.ssh/config

Goal: build reusable tunnel definitions.

What you can do: save common tunnels under a short name.

What it does: move forwarding options into SSH config.

In your SSH config file ~/.ssh/config, you can define a reusable tunnel like this:

Host prod-db-tunnel
    HostName flotte.sh
    User julian
    LocalForward 5432 127.0.0.1:5432
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

This example opens local port 5432 on your machine and forwards it to 127.0.0.1:5432 as seen from flotte.sh. In other words, connecting to localhost:5432 on your machine sends traffic to port 5432 on flotte.sh itself. It also keeps the connection alive and fails fast if the tunnel cannot be established.

This is equivalent to running:

ssh -N -L 5432:127.0.0.1:5432 flotte.sh

Then start it with:

ssh -N prod-db-tunnel

This keeps commands short and easier to share.

You can do the same for a remote forward. For example, to publish a local Ollama instance on port 11434 through flotte.sh:

Host ollama-share
    HostName flotte.sh
    User julian
    RemoteForward 11434 127.0.0.1:11434
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

This opens port 11434 on flotte.sh and forwards that traffic back to 127.0.0.1:11434 on your machine.

This is equivalent to running:

ssh -N -R 11434:127.0.0.1:11434 flotte.sh

Then start it with:

ssh -N ollama-share

Conclusion

  • -L means: open a port on your machine, then send that traffic through SSH to a destination on the remote side
  • -R means: open a port on the remote machine, then send that traffic through SSH back to a destination on your side
  • read the middle part as: listening-port -> destination-host:destination-port
  • if you want to choose which interface the forwarded port listens on, you can use -L <bind-host>:<port>:<destination-host>:<destination-port> or -R <bind-host>:<port>:<destination-host>:<destination-port>