Skip to content
Latchkey

ssh -L: Tunnel a Port Through SSH in CI

ssh -L forwards a local port over an SSH connection, so a CI job can reach a database or API that is only accessible from a bastion host.

Private services often sit behind a bastion. ssh -L tunnels a local port to that service through the bastion, letting tests connect to localhost while the traffic actually flows over SSH to the real backend.

What it does

ssh -L local_port:target_host:target_port bastion opens an SSH session and listens on local_port; connections to it are forwarded over the encrypted session to target_host:target_port as seen from the bastion. The job talks to localhost while SSH carries the bytes to the private service.

Common usage

Terminal
# forward localhost:5432 to db.internal:5432 via the bastion, no shell
ssh -N -f -L 5432:db.internal:5432 user@bastion.example.com
# then wait for the tunnel before connecting
for i in $(seq 1 10); do nc -z localhost 5432 && break; sleep 1; done

Options

FlagWhat it does
-L <lport>:<host>:<rport>Forward a local port to host:rport via the server
-NDo not run a remote command (forwarding only)
-fGo to background after authenticating
-o StrictHostKeyChecking=accept-newAccept and pin a new host key non-interactively
-o ExitOnForwardFailure=yesFail if the forward cannot be set up
-i <keyfile>Identity (private key) to authenticate with

In CI

Use -N -f to background the tunnel, then nc -z localhost <port> in a loop to wait until it is ready before the dependent step runs. Add -o ExitOnForwardFailure=yes so a failed forward kills ssh immediately instead of leaving a connected session with no working tunnel. Pin the host key with StrictHostKeyChecking=accept-new (or a known_hosts file) so the connection does not prompt and hang.

Common errors in CI

"bind: Address already in use" means local_port is taken; another tunnel or service holds it. "channel 1: open failed: connect failed: Connection refused" means the bastion reached target_host but the service there is down. "Host key verification failed" means the host key is unknown and StrictHostKeyChecking blocked it; use accept-new or seed known_hosts. "Permission denied (publickey)" means the wrong key or user.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →