Linux Administration·8 min read·

Local and Remote SSH Port Forwards Explained

Keep -L, -R, and -D straight: local pulls a remote port home, remote publishes a lab port, SOCKS is a dynamic proxy you can still draw on paper.

NB

Netbay Infrastructure Team

Netbay Engineering

On this page

Port forwarding is where SSH stops being a shell and starts being plumbing. The flags are short, the bind addresses are easy to swap, and a year later nobody on the team can say whether 5432 lives on the laptop or the VPS. Local, remote, and dynamic forwards are three different arrows. If you can point to where a TCP listener appears, you can debug the rest.

This post assumes you already have key auth and a Host alias. It does not rehash hardening. It teaches the three arrows so you can still explain them on a call.

Three arrows: local, remote, dynamic -L LOCAL listen on laptop connect on VPS side pull a remote port home -R REMOTE listen on VPS connect on laptop publish a lab port -D SOCKS listen on laptop per-connection dest dynamic proxy Ask: where does the new TCP listener appear? That names the flag. Then ask: where does sshd or the client dial next? That names the target pair. Bind 127.0.0.1 unless a second local process must attach from another machine.

Local forwards: pull a remote port home

ssh -L 127.0.0.1:5432:127.0.0.1:5432 vps. The listener appears on your laptop. When you point psql at 127.0.0.1:5432, the SSH client carries the bytes to the VPS and the VPS connects to 127.0.0.1:5432 there — the database bound on the server loopback. You never opened 5432 on the public interface. You never ran a VPN. You used the SSH session you already had.

The first address pair is local bind. The second is the destination as seen from the remote host. That second fact is the bit people reverse. 127.0.0.1 in the destination is the VPS loopback, not yours. If the database listens on 10.0.0.9 inside a private network the VPS can reach, the destination is 10.0.0.9:5432 and the VPS is the one that must have a route.

bash
ssh -N -L 127.0.0.1:5432:127.0.0.1:5432 deploy@vps
ssh -N -L 127.0.0.1:8443:10.0.0.9:443 deploy@vps

-N again: no shell. Bind 127.0.0.1 so other machines on your coffee-shop LAN cannot use your tunnel. Binding 0.0.0.0 on the laptop is a choice you should be able to defend.

Local forwards are how you reach Redis, Postgres, a private admin UI, and the sshd metrics endpoint without publishing them. They are also how you accidentally dump a production database to a laptop. Treat the local port as production data in transit.

Remote forwards: publish a lab port

ssh -R is the reverse tunnel from the previous post. The listener appears on the VPS. The destination is on the client side. Use it to show a development server, to let a webhook on the VPS hit a local process, or to give a reporting job access to a lab database. Do not use it as production ingress.

The two flags are mirrors. If you remember only one sentence: -L listens locally, -R listens remotely. Everything else is bind address and destination as seen from the side that dials.

Dynamic SOCKS: -D as a per-connection proxy

ssh -D 127.0.0.1:1080 vps opens a SOCKS listener on the laptop. Each connection through that SOCKS port tells the VPS where to dial. A browser or curl --socks5-hostname 127.0.0.1:1080 then originates traffic from the VPS. This is useful for debugging how a site sees a Lucknow address, or for reaching a set of internal HTTP services without a local forward per port.

bash
ssh -N -D 127.0.0.1:1080 deploy@vps
curl --socks5-hostname 127.0.0.1:1080 https://example.com

--socks5-hostname is the variant that sends DNS through the proxy. Without it, your laptop resolves names and only the TCP hop uses SOCKS, which is how you leak name queries and how split-horizon DNS fails.

SOCKS is not a full VPN. UDP, ICMP, and most non-TCP tools will not go through it. Do not set a system-wide proxy you forget about. Do not leave -D bound on 0.0.0.0.

sshd knobs that actually gate this

AllowTcpForwarding yes is the default. Set it to local if a host may originate local-style forwards (from the server's point of view, remote listener is the other one — read the man page twice). PermitOpen restricts destinations a client may ask the server to dial, which is how a bastion is allowed to reach 10.0.0.0/24:22 and nothing else. DisableForwarding yes is the hammer for SFTP-only or git-only accounts.

ini
AllowTcpForwarding yes
PermitOpen 10.0.0.0/24:22 10.0.0.9:5432
AllowStreamLocalForwarding no
GatewayPorts no

Match blocks, in the next post, let you give humans forwarding and deny it for deploy-bot. Do not try to express that with one global PermitOpen that you then exception by hope.

Put the forwards in ~/.ssh/config with LocalForward, RemoteForward, and DynamicForward so the command is ssh db-tunnel, not a flag soup. ExitOnForwardFailure yes still applies. Combine with ControlMaster so the tunnel and a shell share one handshake on a slow link.

A short takeaway: name the listener first, then the dial, then the bind address. -L pulls home, -R publishes, -D is SOCKS. Keep listeners on loopback and keep production ports off coffee-shop interfaces. Open an Ubuntu 24.04 VPS on Netbay in under 60 seconds and practice all three arrows against loopback services — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance