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