SSH remote port forwarding is actually a way to punch holes from a remote server back into your local network, not the other way around.
Let’s say you have a web server running on your laptop at localhost:8080, and you want to make it accessible from the internet, but your laptop is behind a NAT firewall and doesn’t have a public IP. You can use a publicly accessible server (let’s call it jump.example.com) to act as a bridge.
Here’s how you’d set it up:
On your laptop, you run:
ssh -R 8080:localhost:8080 user@jump.example.com
Now, if you SSH into jump.example.com and run curl localhost:8080, you’ll see the output from your laptop’s web server. The -R flag tells the SSH client on your laptop to tell the SSH server on jump.example.com to listen on port 8080 on jump.example.com’s network interface, and forward any traffic received on that port back through the SSH tunnel to your laptop, where it will be directed to localhost:8080.
The problem this solves is exposing services running on a machine that’s not directly reachable from the internet. Think of development servers, internal dashboards, or even a temporary file share.
Internally, it’s a bit of a clever trick. When you establish an SSH connection, it’s normally for you to ssh into the remote machine. The -R flag modifies this. The SSH client on your local machine essentially says to the SSH server on jump.example.com: "Hey, can you start listening on port 8080 on your end? And anything that comes in, send it back to me through this tunnel, and I’ll forward it to localhost:8080." The SSH server then creates a listening socket on its specified port. When a connection arrives, the SSH server reads the data, sends it over the established SSH tunnel to your SSH client, which then writes that data to the target localhost:8080.
The exact levers you control are the ports. The first port (8080 in -R 8080:localhost:8080) is the port the remote server will listen on. The second port (localhost:8080) is the destination on your local machine. You can forward any local service to any available port on the remote server. For instance, if you wanted to expose a PostgreSQL database running on your laptop (localhost:5432) to a remote administrator who can access jump.example.com, you’d use:
ssh -R 54320:localhost:5432 user@jump.example.com
The administrator would then connect to jump.example.com on port 54320.
A common pitfall is that by default, the remote port will only be bound to the loopback interface (127.0.0.1) of the remote server. This means only processes running on jump.example.com itself can connect to that forwarded port. To make it accessible from other machines on jump.example.com’s network, you need to tell the SSH server to bind to 0.0.0.0 (all interfaces) or a specific IP address. This is often done by modifying the GatewayPorts option in the sshd_config file on the remote server (jump.example.com), setting it to yes. After changing this, you’d restart the SSH server on jump.example.com (sudo systemctl restart sshd or similar). Then, the command on your laptop would look like:
ssh -R \*:8080:localhost:8080 user@jump.example.com
The \*: prefix on the remote port tells the SSH server to bind to all available network interfaces.
If you try to use ssh -R and the remote port is already in use, you’ll get an "Address already in use" error. This is because another process on the remote server is already listening on that port. You’ll need to choose a different port for the remote listener or stop the conflicting process on the remote server.
The next concept you’ll likely run into is securing these forwarded connections, as exposing a local service to the internet, even indirectly, carries risks.