I’m trying to understand SSH tunneling for secure networking, including local, remote, and dynamic port forwarding. I’ve seen SSH tunnels described as a networking Swiss Army knife, but I need help knowing when and how to use each type.
The easy mistake is exposing a forwarded port to more machines than intended. SSH tunnels are encrypted between your computer and the SSH server, but whatever happens beyond the server is normal network traffic. Bind local forwards to 127.0.0.1 unless other devices genuinely need access.
The three common types differ mainly in where the listening port is created:
- Local forwarding:
ssh -N -L 127.0.0.1:8080:internal.example:80 user@gateway
Port 8080 opens on your computer. Connecting to localhost:8080 sends traffic through gateway, which then connects to internal.example:80. Use this when you can SSH into a network but cannot directly reach a database, web dashboard, or other internal service.
- Remote forwarding:
ssh -N -R 127.0.0.1:9000:localhost:3000 user@server
Port 9000 opens on the SSH server and points back to port 3000 on your computer. This is useful for temporarily letting something on the remote machine reach a local development server. By default, it is usually reachable only from the remote machine itself. Making it public may require a different bind address plus the server’s GatewayPorts setting, and that deserves care because it can expose your local service.
- Dynamic forwarding:
ssh -N -D 127.0.0.1:1080 user@gateway
This creates a SOCKS proxy on your computer. Instead of forwarding one fixed destination, SOCKS-aware applications can request different destinations through the SSH server. Configure the application to use SOCKS5 at 127.0.0.1:1080. If privacy matters, check whether the application sends DNS lookups through the proxy too, since some clients otherwise leak DNS requests outside the tunnel.
-N means “do not run a remote command,” which is appropriate for tunnel-only sessions. -T disables terminal allocation, and -o ExitOnForwardFailure=yes makes SSH quit if it cannot create the requested listener. A practical command might be ssh -NT -o ExitOnForwardFailure=yes -L 127.0.0.1:5433:db.internal:5432 user@gateway.
A tunnel does not bypass authentication on the destination service, and it is not a full VPN. Local forwarding handles chosen ports, while dynamic forwarding only covers applications configured for SOCKS. Start with local forwarding because its traffic path is easiest to reason about, then use remote forwarding only when you specifically need the connection direction reversed.
Treat the tunnel as a temporary cable, not a permanent service. A useful habit is adding -v while learning so SSH shows where the listener was created and what connection failed. If a tunnel will run unattended, use keepalives such as -o ServerAliveInterval=30 -o ServerAliveCountMax=3, otherwise a dead session can look alive while traffic quietly hangs.
Don’t bind a tunnel to 0.0.0.0 unless you intend other machines to use it, since that can expose the forwarded service to your network. In short, -L brings a remote service to your machine, -R exposes your local service through the SSH server, and -D creates a SOCKS proxy. @xdarkstackx’s -v suggestion helps confirm which address and port SSH actually opened.
If localhost appears as the destination in a local forward, it means “localhost from the SSH server.” In a remote forward, it means “localhost from your computer.” That change in perspective is where many otherwise correct-looking tunnel commands go wrong.
A simple way to read -L is from left to right: create a listener here, then connect to the destination from there. For example:
ssh -NT -L 127.0.0.1:15432:127.0.0.1:5432 user@db-server
First, SSH opens port 15432 on your computer. When you connect to it, SSH carries the connection to db-server. The SSH server then connects to its own 127.0.0.1:5432. This works well when PostgreSQL is deliberately listening only on the server’s loopback interface. Your database client still needs the normal database username and password. SSH only provides the path.
With -R, reverse the locations:
ssh -NT -R 127.0.0.1:18000:127.0.0.1:8000 user@build-server
First, SSH opens port 18000 on build-server. A connection to that port travels back through SSH, then your computer connects to its own port 8000. If you are running a temporary local web server there, someone logged into build-server can access it at 127.0.0.1:18000. As the earlier replies noted, keeping the remote listener on loopback avoids accidentally publishing your local service to the rest of that network.
Dynamic forwarding is different because there is no destination in the command:
ssh -NT -D 127.0.0.1:1081 user@gateway
SSH opens a SOCKS proxy locally, and the application chooses each destination. This is convenient for a browser or command-line program that understands SOCKS. It does not automatically redirect all traffic from your computer, and it is primarily for TCP. Programs that require UDP, or programs with no proxy setting, may need a VPN instead. For DNS, look for an option such as “proxy DNS through SOCKS.” Some tools express that distinction as socks5h rather than socks5.
When a forward fails, test it in this order. For -L, log into the SSH server and confirm that the destination is reachable from there. For -R, confirm that the destination is reachable from your own computer. Then run SSH with -v or -vv, check that the expected listening port exists, and finally test the listener with the actual client. This separates “SSH did not create the tunnel” from “the tunnel exists, but the destination service is down or listening on a different address.”
Another common surprise is that SSH may successfully log in even though the forwarding request failed. -o ExitOnForwardFailure=yes prevents that misleading half-working state. It catches listener setup failures, such as a port already being occupied. It cannot guarantee that the final database or web service is reachable, because those connections happen later when you actually use the tunnel.
The shortest decision rule is: use -L when you are sitting here and need to reach something over there, use -R when something over there needs to reach a service sitting here, and use -D when a proxy-aware application needs to choose several destinations. Then check whose point of view each localhost belongs to before running the command.
Successful SSH login does not mean tunneling is allowed. The server can disable forwarding entirely or restrict which destinations and ports you may reach, so a correct -L or -R command can still be rejected by policy.
Watch the SSH output rather than assuming the application is broken. Run with -vv and look for messages such as “administratively prohibited.” That usually points to server settings like AllowTcpForwarding, PermitOpen, or remote-forwarding restrictions, not a typo in your browser or database client.
There is another access-control wrinkle: the destination normally sees the connection as coming from the SSH server, not your laptop. That can make a tunnel work where direct access fails, but it can also affect firewall rules, rate limits, and audit logs. Treat the SSH host as an active relay with its own permissions, not merely an encrypted extension cable.
Do not disable host-key checking just to make the connection work. An encrypted tunnel is useless if you have not verified which SSH server you connected to. If the host key unexpectedly changes, investigate it instead of blindly deleting the warning or adding StrictHostKeyChecking=no.
Once you understand the direction, stop keeping giant commands in shell history. Put repeatable tunnels in ~/.ssh/config so the bind address, ports, and safety options do not get mistyped:
Host work-db
HostName gateway.example
User alice
LocalForward 127.0.0.1:15432 db.internal:5432
ExitOnForwardFailure yes
ServerAliveInterval 30
ServerAliveCountMax 3
Then start it with:
ssh -NT work-db
The same config syntax supports RemoteForward and DynamicForward. Use separate host aliases for separate jobs instead of stuffing every forward into one connection. That makes it obvious what you are exposing and lets you shut down one tunnel without taking unrelated access with it.
Be clear about the two security boundaries. SSH protects traffic between your machine and the SSH server. The SSH server then makes the destination connection under its own network identity. Your database password, web login, TLS settings, and destination firewall rules still matter. As @0xbadger2 pointed out, the server can reject forwarding by policy, and no client-side option overrides that.
Finally, confirm the actual listener instead of trusting a successful login. Check that the chosen port is listening only on 127.0.0.1, connect through it, and watch the verbose SSH output if it fails. If you later turn the tunnel into an unattended service, use a dedicated SSH account with restricted forwarding permissions. A general shell account running a permanent tunnel is more access than most setups need.
A tunnel you sit and watch in a terminal behaves completely differently from one you expect to survive a laptop sleep or a wifi hiccup. @shadowhive4550’s keepalive tip is right, but worth being clear about what it actually does: ServerAliveInterval kills a dead session faster, it doesn’t reconnect anything. Once the link drops, your listener is gone until you run the command again. If you want something that comes back on its own, that’s autossh or a systemd unit with restart, not SSH flags. For quick debugging or a one-off port grab, don’t bother with any of that. The config-block approach @beaconhq showed is the sweet spot for tunnels you reopen often but still start by hand.
Reaching one specific port behind a bastion is a job for -L. Wanting to actually SSH into a machine that sits behind that bastion is a job for ProxyJump, and people constantly reach for a chain of local forwards when -J (or a ProxyJump line in the config @beaconhq showed) would be cleaner and less fragile. That distinction gets blurred in a lot of tunnel guides. If your end goal is a shell on the inner host, stacking -L forwards just to expose port 22 is the awkward way to do it.
The other thing worth flagging: dynamic forwarding with a browser feels great until you realize plenty of ‘SOCKS support’ in desktop apps is half-baked and quietly ignores the proxy for some requests. @codecrafter’s socks5h point is the real tell there. When in doubt, watch the SSH server’s own network, not your laptop, to confirm where traffic is actually coming out.