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 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:
- Local port forwarding (
-L) — a local port forwards to a service reachable from the remote side. - Remote port forwarding (
-R) — a remote port forwards to a service reachable from the local side. - 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:3000Remote forwarding
Remote Server
localhost:8080
│
│ SSH tunnel
▼
Local Machine
localhost:3000With -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_SERVERThe easiest way to think about it is:
-R REMOTE_PORT:DESTINATION_HOST:DESTINATION_PORTCreate 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:2399You want a remote server to be able to access that application.
Create the reverse tunnel:
$ ssh -N -R 2393:localhost:2399 user@REMOTE_SERVER_IPSSH establishes the connection from your machine to:
REMOTE_SERVER_IPand creates a listening port:
REMOTE_SERVER_IP:2393The traffic flow becomes:
Remote Server
localhost:2393
│
│ SSH tunnel
▼
Your Local Machine
localhost:2399
│
▼
Web ApplicationNow, a process running on the remote server can connect to:
localhost:2393and its traffic is carried through the existing SSH connection to:
localhost:2399on your local machine.
The important distinction
The SSH connection itself is:
Your Machine ───────────► Remote ServerBut the forwarded traffic flows:
Remote Server ──────────► Your MachineThat 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 ServerYour 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 LaptopNo 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_SERVERThe remote server listens on port 8080, while traffic is forwarded through the SSH connection to:
10.10.20.50:80from 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:80The 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_SERVERtypically creates:
Remote Server
127.0.0.1:8080rather than:
Remote Server
0.0.0.0:8080This 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_configThe 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:8080GatewayPorts yes
Allows remote forwarded ports to bind to wildcard addresses.
For example:
0.0.0.0:8080If 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_SERVERThis requests that the remote forwarding listen specifically on:
15.152.66.244:8080rather 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 accessIf 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_SERVERThe remote server now has:
127.0.0.1:2393forwarding to:
Your Machine → localhost:2399You can verify the listening port on the remote server with:
$ ss -lntpKeeping 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_SERVERFor 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_SERVERKeepalives 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_SERVERThen verify the remote listening socket:
$ ss -lntpOn the remote server, test the forwarded port:
$ curl http://127.0.0.1:2393If 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:2399The 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@serverRemote :8080 → Local :3000Explicit loopback binding
$ ssh -N -R 127.0.0.1:8080:localhost:3000 user@serverForward to another local-network host
$ ssh -N -R 8080:10.10.20.50:80 user@serverBackground tunnel
$ ssh -f -N -R 127.0.0.1:8080:localhost:3000 user@serverDebugging
$ ssh -vvv -N -R 127.0.0.1:8080:localhost:3000 user@serverThe Mental Model
If -L means:
-L
Local listener
│
▼
Remote destinationthen -R means:
-R
Remote listener
│
▼
Local destinationA 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 ServerBut with remote forwarding, connections arriving at the remote listener are carried back through that SSH session:
Remote listener
│
▼
SSH tunnel
│
▼
Local destinationOnce 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_PORTCreate 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!