Which one do you need?
| Flag | Port opens on | Use it to |
|---|---|---|
-L local | Your machine | Reach a service that only the server can reach, such as a database. |
-R remote | The server | Let the server reach something on your machine, such as a dev site. |
-D dynamic | Your machine | Browse or connect to many hosts as if you were on the server. |
An easy way to remember: -L listens on your local machine, -R listens on the remote server, and -D is dynamic because the app picks the destination each time.
Local forward: -L
ssh -N -L 8080:localhost:80 user@server
your app -> your machine, port 8080 -> SSH -> server -> localhost:80 as seen by the server
The format is local_port:destination:destination_port. Now http://127.0.0.1:8080 on your machine shows the web server that listens only on the server's own loopback address.
The key point is that the server resolves the destination. localhost means the server itself, not your machine. The destination can also be another host the server can reach, and that last hop is not encrypted by SSH:
ssh -N -L 8080:intranet.example.com:80 user@server
Remote forward: -R
ssh -N -R 9000:localhost:3000 user@server
a program on the server -> server, port 9000 -> SSH -> your machine -> localhost:3000 as seen by your machine
Now a program on the server can open http://127.0.0.1:9000 and reach the app running on your machine. It works even when your machine is behind NAT, because your side started the connection.
Dynamic forward: -D
ssh -N -D 1080 user@server
your app -> SOCKS proxy on your machine, port 1080 -> SSH -> server -> any host:port the app asks for
Point an application that supports SOCKS at 127.0.0.1:1080. To test with curl:
curl --socks5-hostname 127.0.0.1:1080 http://intranet.example.com/
--socks5-hostname makes the server do the DNS lookup, so internal names work and the lookup does not leak from your own network. Plain --socks5 resolves the name on your machine.
Browser settings and DNS leaks are covered in how to use SSH as a SOCKS proxy.
Several ports, or through a jump host
Repeat the option to open several forwards over one connection:
ssh -N -L 127.0.0.1:8080:localhost:80 -L 127.0.0.1:5433:localhost:5432 user@server
When the server sits behind a bastion, add -J. The forward starts at the last server of the chain, so that server resolves the destination:
ssh -N -J user@bastion -L 127.0.0.1:5433:localhost:5432 user@db-server
Chains of servers are covered in how to use ProxyJump and a jump host.
Running a tunnel without a shell
-Nruns no remote command. The session only carries the tunnel.-fgoes to the background after login, so you can still type a passphrase first.-o ExitOnForwardFailure=yesexits with an error if the forward cannot be set up.
ssh -f -N -o ExitOnForwardFailure=yes -L 127.0.0.1:8080:localhost:80 user@server
Without ExitOnForwardFailure, ssh prints a warning and stays connected with no working tunnel, which is confusing inside a script. With it, a local port already in use ends with exit code 255 and Could not request local forwarding, and a used remote port ends with remote port forwarding failed for listen port.
One catch, seen in our tests: with a bare -L 8080:..., ssh listens on both 127.0.0.1 and ::1. If only one of those is taken, for example by another program on IPv4, ssh prints bind [127.0.0.1]:8080: Address already in use and keeps running even with ExitOnForwardFailure=yes. Your app may then reach that other program. Writing the address, as in -L 127.0.0.1:8080:..., makes this a real failure, so use that form in scripts.
To stop a background tunnel, find its process and kill it:
pgrep -f 'L 127.0.0.1:8080'
kill PID
For tunnels you start often, a control socket lets you stop them by name with ssh -O exit. See how to save, start and stop SSH tunnels.
To keep a tunnel in your config, use LocalForward, RemoteForward and DynamicForward:
Host dbtunnel
HostName 203.0.113.10
User user
LocalForward 127.0.0.1:8080 localhost:80
ExitOnForwardFailure yes
Bind to localhost only
A forwarded port is open to whoever can reach it, and the service behind it may trust connections from the server. By default, ssh binds to loopback only, so keep it that way unless you have a reason.
These commands open the local port to your whole network. We confirmed both listen on all interfaces:
ssh -N -L 0.0.0.0:8080:localhost:80 user@server
ssh -N -g -L 8080:localhost:80 user@server
For -R, the server decides, with GatewayPorts in its sshd configuration:
| GatewayPorts | -R 9000:... | -R 0.0.0.0:9000:... |
|---|---|---|
no (default) | Loopback only | Loopback only, with no error |
clientspecified | Loopback only | All interfaces |
yes | All interfaces | All interfaces, and 127.0.0.1:9000 also opens on all interfaces |
So if your -R port cannot be reached from outside, GatewayPorts is the usual reason. Exposing your laptop's service to the internet this way is rarely what you want. Prefer clientspecified if you need it, and add a firewall rule.
When the server refuses
AllowTcpForwarding in the server's sshd configuration controls this. The values are yes (the default), no, local (allows -L and -D) and remote (allows -R).
- A blocked
-Lor-Dconnects fine. The error comes only when something uses the port:channel 2: open failed: administratively prohibited: open failed. - A blocked
-Rsaysremote port forwarding failed for listen port.
The manual page notes that disabling forwarding adds little if users also have a shell, since they can run their own forwarder. To truly limit someone, take away the shell too, as in a tunnel-only user for a database.
Two more errors come up often. When the local or remote port is taken, see Address already in use and remote port forwarding failed. When connections through the tunnel are refused, see administratively prohibited: open failed.
Questions
What is the difference between ssh -L and ssh -R?
-L opens a port on your machine that leads to something the server can reach. -R opens a port on the server that leads back to something on your machine.
How do I forward several ports with one ssh command?
Repeat the option: ssh -N -L 127.0.0.1:8080:localhost:80 -L 127.0.0.1:5433:localhost:5432 user@server. One connection carries all of them. In ~/.ssh/config, add one LocalForward line per port.
How do I keep an SSH tunnel running?
Start it with -N and -o ExitOnForwardFailure=yes, and let autossh or a systemd service restart it after a drop. See autossh and systemd.
Why does my tunnel connect but nothing works?
The server may forbid forwarding with AllowTcpForwarding. A blocked -L or -D still connects, and fails only when the port is used, with administratively prohibited: open failed.
Is SSH port forwarding secure?
The traffic is encrypted between your machine and the SSH server. The hop from the server to the destination is not, and a port that listens on 0.0.0.0 can be used by anyone on your network, so keep the listen address at 127.0.0.1.