SSH Port Forwarding ⤳ aka SSH Tunneling — Remote Port Forwarding (Part 2)

Learn how SSH remote port forwarding securely makes local services accessible from remote systems. Explore ssh -R, reverse SSH tunnels, NAT and firewall traversal, GatewayPorts, security considerations, and practical examples.

SSH Port Forwarding ⤳ aka SSH Tunneling — Remote Port Forwarding (Part 2)
Photo by yannick Coffi see +< @ yannickcoffi @ PRINTS




SSH Tunneling — Part 2: Remote Port Forwarding

How reverse SSH tunnels make local services accessible from remote systems

In Part 1, we used local port forwarding (-L) to access a remote service from our local machine.

This time, we reverse the direction.

Remote port forwarding (-R) allows a service running on your local machine—or on a host reachable from it—to be accessed through a listening port on the remote SSH server.

This is commonly called a reverse SSH tunnel.

How SSH Port Forwarding Works

SSH supports three primary forwarding modes:

  1. Local port forwarding (-L) — a local port forwards to a service reachable from the remote side.
  2. Remote port forwarding (-R) — a remote port forwards to a service reachable from the local side.
  3. Dynamic port forwarding (-D) — creates a SOCKS proxy that dynamically forwards connections through the SSH server.

The key difference between -L and -R is where the listening port is created.

Local forwarding

Local Machine
localhost:8080
      │
      │ SSH tunnel
      ▼
Remote Server
localhost:3000

Remote forwarding

Remote Server
localhost:8080
      │
      │ SSH tunnel
      ▼
Local Machine
localhost:3000

With -R, the SSH connection is still established from the local machine to the remote server. What changes is where SSH creates the listening port.

Remote Port Forwarding (-R)

Remote port forwarding allows you to expose a service reachable from your local machine through a port on the remote SSH server.

The basic syntax is:

$ ssh -N -R [REMOTE_BIND_ADDRESS:]REMOTE_PORT:DESTINATION_HOST:DESTINATION_PORT user@REMOTE_SERVER

The easiest way to think about it is:

-R REMOTE_PORT:DESTINATION_HOST:DESTINATION_PORT
Create a listening port on the remote side and send its traffic through SSH to the destination on the local side.

A Practical Example

Suppose you have a web application running on your local machine:

localhost:2399

You want a remote server to be able to access that application.

Create the reverse tunnel:

$ ssh -N -R 2393:localhost:2399 user@REMOTE_SERVER_IP

SSH establishes the connection from your machine to:

REMOTE_SERVER_IP

and creates a listening port:

REMOTE_SERVER_IP:2393

The traffic flow becomes:

Remote Server
localhost:2393
      │
      │ SSH tunnel
      ▼
Your Local Machine
localhost:2399
      │
      ▼
Web Application

Now, a process running on the remote server can connect to:

localhost:2393

and its traffic is carried through the existing SSH connection to:

localhost:2399

on your local machine.

The important distinction

The SSH connection itself is:

Your Machine ───────────► Remote Server

But the forwarded traffic flows:

Remote Server ──────────► Your Machine

That is why this is commonly called a reverse tunnel.

The SSH session does not reverse direction. The forwarded connection does.

Why Reverse SSH Tunneling Is Useful

Remote port forwarding is particularly useful when the machine running the service cannot accept inbound connections.

For example:

Your Laptop
     │
     │ outbound SSH connection
     ▼
Remote Server

Your laptop might be behind:

  • NAT
  • A home router
  • A corporate firewall
  • CGNAT
  • A restrictive network

Normally, the remote server cannot establish a direct connection back to your laptop.

But if your laptop can establish an outbound SSH connection, it can create a reverse tunnel:

Your Laptop
     │
     │ SSH connection
     ▼
Remote Server
     │
     │ remote forwarded port
     ▼
     └──────────► SSH tunnel ──────────► Your Laptop

No inbound connection to your laptop is required.

This makes reverse tunneling useful for remote development, testing, temporary access, and reaching services behind restrictive network boundaries.

The Destination Does Not Have to Be localhost

Just like local forwarding, the destination does not have to be localhost.

For example:

$ ssh -N -R 8080:10.10.20.50:80 user@REMOTE_SERVER

The remote server listens on port 8080, while traffic is forwarded through the SSH connection to:

10.10.20.50:80

from the local side's network perspective.

This can be useful when your local machine can reach an internal service that the remote server cannot reach directly.

For example:

Remote Server
     │
     │ connects to :8080
     ▼
Remote SSH Forward
     │
     │ SSH tunnel
     ▼
Your Local Network
     │
     ▼
10.10.20.50:80

The SSH client acts as the bridge between the two network contexts.

Controlling Who Can Access the Remote Port

There is an important security detail with -R.

By default, OpenSSH binds remote forwarded ports to the loopback interface. In practice, this usually means the forwarded port can only be accessed from the remote server itself.

For example:

$ ssh -N -R 8080:localhost:2399 user@REMOTE_SERVER

typically creates:

Remote Server
127.0.0.1:8080

rather than:

Remote Server
0.0.0.0:8080

This default is important because it prevents a reverse tunnel from automatically becoming a publicly accessible service.

GatewayPorts

The SSH server's GatewayPorts setting controls whether remote forwarded ports can be accessed from other hosts.

It is configured in:

/etc/ssh/sshd_config

The main options are:

GatewayPorts no

This is the default.

Remote forwarding is bound to the loopback interface, so the forwarded port is normally accessible only from the SSH server itself.

127.0.0.1:8080

GatewayPorts yes

Allows remote forwarded ports to bind to wildcard addresses.

For example:

0.0.0.0:8080

If the server is publicly reachable, this can make the forwarded service accessible from the Internet.

This should therefore be enabled only when there is a clear requirement and appropriate network controls are in place.

GatewayPorts clientspecified

Allows the SSH client to specify the address on which the remote forwarded port should listen.

For example:

$ ssh -N -R 15.152.66.244:8080:localhost:80 user@REMOTE_SERVER

This requests that the remote forwarding listen specifically on:

15.152.66.244:8080

rather than all interfaces.

Whether this works depends on the SSH server configuration and the address being available on the remote host.

Remote Forwarding and Security

A reverse tunnel can be extremely useful—but it can also unintentionally turn a private service into a remotely accessible one.

For example:

Private Service
localhost:2399
       │
       │ SSH tunnel
       ▼
Public Server
:8080
       │
       ▼
Potential external access

If the remote forwarded port is bound to a public interface, anyone who can reach that port may potentially reach the service behind the tunnel.

For this reason:

  • Keep remote forwards bound to loopback unless external access is required.
  • Restrict which users are allowed to create forwarding tunnels.
  • Use firewall rules to limit access where appropriate.
  • Monitor persistent SSH connections.
  • Avoid exposing administrative interfaces or sensitive services unnecessarily.
  • Use SSH authentication controls and least privilege.

A reverse tunnel should be treated as a network access mechanism, not simply as a convenient port mapping.

Running a Reverse Tunnel in the Background

For a tunnel that does not require an interactive shell:

$ ssh -f -N \
  -R 127.0.0.1:2393:localhost:2399 \
  user@REMOTE_SERVER

The remote server now has:

127.0.0.1:2393

forwarding to:

Your Machine → localhost:2399

You can verify the listening port on the remote server with:

$ ss -lntp

Keeping a Reverse Tunnel Alive

For long-running reverse tunnels, SSH keepalives can help detect broken connections:

$ ssh -N \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -R 127.0.0.1:2393:localhost:2399 \
  user@REMOTE_SERVER

For persistent environments, autossh can automatically restart a failed SSH connection:

$ autossh -M 0 -N \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -R 127.0.0.1:2393:localhost:2399 \
  user@REMOTE_SERVER

Keepalives help detect a dead connection; autossh adds automatic recovery when the SSH session terminates.

Troubleshooting

If the reverse tunnel does not work, start with verbose SSH output:

$ ssh -vvv -N \
  -R 127.0.0.1:2393:localhost:2399 \
  user@REMOTE_SERVER

Then verify the remote listening socket:

$ ss -lntp

On the remote server, test the forwarded port:

$ curl http://127.0.0.1:2393

If the remote port is listening but the connection fails, verify that the destination service is reachable from the machine running the SSH client:

$ curl http://127.0.0.1:2399

The diagnostic path is therefore:

Remote listener works?
        │
        ▼
SSH connection works?
        │
        ▼
Local destination reachable?
        │
        ▼
Application responding?

This helps isolate whether the problem is with SSH, forwarding, networking, or the application itself.

Quick Reference

Basic reverse tunnel

$ ssh -N -R 8080:localhost:3000 user@server
Remote :8080 → Local :3000

Explicit loopback binding

$ ssh -N -R 127.0.0.1:8080:localhost:3000 user@server

Forward to another local-network host

$ ssh -N -R 8080:10.10.20.50:80 user@server

Background tunnel

$ ssh -f -N -R 127.0.0.1:8080:localhost:3000 user@server

Debugging

$ ssh -vvv -N -R 127.0.0.1:8080:localhost:3000 user@server

The Mental Model

If -L means:

-L

Local listener
      │
      ▼
Remote destination

then -R means:

-R

Remote listener
      │
      ▼
Local destination

A simple way to remember it:

-L  → Listen here, reach there.
-R  → Listen there, reach here.

The SSH connection itself still originates from the local machine:

Local ───────────► SSH Server

But with remote forwarding, connections arriving at the remote listener are carried back through that SSH session:

Remote listener
      │
      ▼
SSH tunnel
      │
      ▼
Local destination

Once this distinction is clear, -R becomes much easier to reason about.

What’s Next?

Remote forwarding moves traffic from a remote system toward a service on your local side.

In this tutorial, we used remote port forwarding (-R) to make a local service accessible through a remote SSH server—without requiring a direct inbound connection to the local machine.

The key idea is simple:

-R REMOTE_PORT:DESTINATION_HOST:DESTINATION_PORT

Create the listener on the remote side, and carry the traffic back through the existing SSH connection to the destination on the local side.

In the next part, we move beyond fixed port mappings with dynamic port forwarding (-D) and SOCKS proxies, allowing applications to dynamically route TCP connections through an SSH server.

As we wrap up this tutorial, I’d like to sincerely thank everyone who took part and contributed along the way. Your questions, ideas, and curiosity are what make learning truly valuable.

Learning is a collaborative journey, so keep exploring, keep asking questions, and don’t be afraid to dig deeper.

Thank you again for being part of it, and best of luck on the journey ahead!

Keep Us Caffeinated  ⦿ ⦿
Spotify Logo
SNUBmonkey Join our readers 37K+