Files, tunnels, and jumps
Create local and remote tunnels
Map listening and destination endpoints for local and remote port forwarding before opening them.
Postgres on our server listens only on 127.0.0.1:5432. That is the right setup: the database is not on the internet. But now I want to open it in a GUI client on my laptop. The wrong fix is opening port 5432 in the firewall. The right fix is a tunnel through the SSH connection I already trust.
Four endpoints, every time
A port forward always involves two machines and two ports. Before typing any flag, write all four down:
- where SSH listens
- on which machine
- where it sends the traffic
- as seen from which machine
Do this on paper first. Every tunnel bug I have ever debugged was one of those four being wrong.
Local forwarding
-L makes a port on your laptop lead to a destination the server can reach:
ssh -N -o ExitOnForwardFailure=yes -L 5433:127.0.0.1:5432 notes-server
Read -L as local_port:destination_as_seen_from_server:destination_port. SSH listens on 127.0.0.1:5433 on my laptop. Anything that connects there travels through SSH and comes out on the server, aimed at 127.0.0.1:5432. That second 127.0.0.1 is the server’s loopback, not mine.
-N means “no remote command, just forward”. ExitOnForwardFailure=yes makes SSH quit if it cannot open the listener, instead of silently connecting without the tunnel.
Now, in another terminal:
psql -h 127.0.0.1 -p 5433 -U notes notes
It connects, and the database still has no public port.
Prove nobody else can use it
The local listener is bound to loopback by default. Check:
lsof -nP -iTCP:5433 -sTCP:LISTEN
COMMAND PID USER FD TYPE NODE NAME
ssh 4821 flavio 5u IPv4 TCP 127.0.0.1:5433 (LISTEN)
127.0.0.1:5433, not *:5433. From another machine on your network, nc -vz 192.168.1.20 5433 fails. That is the evidence.
The dangerous variant is -L 0.0.0.0:5433:127.0.0.1:5432, or GatewayPorts yes in your config. Now the whole office has a path to your production database, through your laptop, authenticated as you. Bind narrowly unless you can name who else needs it and why.
Remote forwarding
-R goes the other way. Something on your laptop becomes reachable from the server:
ssh -N -R 8080:127.0.0.1:3000 notes-server
A process on the server connecting to 127.0.0.1:8080 reaches port 3000 on your laptop. I use this for testing webhooks against a dev server. The server’s sshd decides whether remote forwards are allowed at all, and whether they may bind beyond loopback.
One realistic failure
Run the -L command twice. The second one prints:
bind [127.0.0.1]:5433: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 5433
Could not request local forwarding.
Without ExitOnForwardFailure, that second ssh would stay connected and look fine, while every query still goes through the first one. That is why I always set the option.
Lesson completed