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
# 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; doneOptions
| Flag | What it does |
|---|---|
| -L <lport>:<host>:<rport> | Forward a local port to host:rport via the server |
| -N | Do not run a remote command (forwarding only) |
| -f | Go to background after authenticating |
| -o StrictHostKeyChecking=accept-new | Accept and pin a new host key non-interactively |
| -o ExitOnForwardFailure=yes | Fail 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.