ShellYard

May 27, 2026 · ssh · networking · bastion · port-forwarding · devops

Reaching Private Infrastructure Through SSH: Port Forwarding Explained

A practical guide to SSH local forwarding, dynamic SOCKS proxies, and ProxyJump — how to reach services on private networks through a bastion, and when to use each.

By ShellYard

Most of the interesting infrastructure you work with isn't sitting on the open internet. Databases, internal APIs, admin dashboards, Windows boxes — they live on private subnets behind a bastion or jump host, reachable only from inside the environment. The one door you have is SSH.

The good news is that SSH does far more than give you a remote shell. The same encrypted connection can carry other traffic, which means you can use a session you already have to reach services your laptop otherwise can't touch. There are three mechanisms worth knowing, and the trick is matching the right one to the job.

Local forwarding (ssh -L): reach one specific service

Local forwarding maps a port on your machine to a specific host and port on the far side of the bastion:

ssh -L 8080:internal-api:8080 user@bastion

This listens on localhost:8080 and forwards anything that hits it through the bastion to internal-api:8080 — where internal-api is resolved from the bastion's perspective. You then point your client (a browser, an HTTP client, a database tool) at localhost:8080.

Use it when you have one known service to reach. The catch: the local end is localhost, so anything that depends on the real hostname — TLS certificate validation, host-based routing, redirects — can behave differently, and you have to track which local port maps to what.

Dynamic forwarding (ssh -D): reach many hosts through one tunnel

Dynamic forwarding turns your SSH session into a SOCKS proxy:

ssh -D 1080 user@bastion

Now anything that can speak SOCKS can route through the bastion. With curl:

curl --proxy socks5h://localhost:1080 http://internal-api:8080/health

The h in socks5h is the important part — it resolves DNS through the proxy, on the bastion's side, so internal hostnames resolve and you keep the real host instead of rewriting everything to localhost. Use it when you need to reach many internal hosts rather than one fixed port. The trade-off: your client has to support SOCKS, and you're configuring proxy settings per tool.

ProxyJump (ssh -J): get through one or more hops

The first two forward a port. ProxyJump is different — it gets your SSH connection itself through one or more intermediate hosts to a deeper target:

ssh -J user@bastion user@app-host

For a chain of bastions, comma-separate them:

ssh -J user@bastion1,user@bastion2 user@app-host

This replaces the older ProxyCommand + netcat pattern you'll still see in the wild, and it's also more secure than forwarding your SSH agent through each hop, since your agent never gets exposed to the intermediate machines. Use it when the host you actually want is two or more hops in.

You can combine ProxyJump with port forwarding — reach the deep host and forward a service off it in one command:

ssh -J user@bastion1,user@bastion2 -L 8080:internal-api:8080 user@app-host

The one to know about but rarely want: reverse forwarding (ssh -R)

For completeness: ssh -R does the opposite — it exposes a service on your machine to the remote side. It's the "publish outward" direction, closer to what a tool like ngrok does. Most of the time, when you're trying to reach private infrastructure, you want the inbound mechanisms above, not this one.

The common thread — and the friction

All three are SSH doing exactly what it's designed to do, and for a one-off they're perfectly fine. The friction shows up when this becomes routine: you're remembering port mappings, fighting localhost rewriting, configuring SOCKS per tool, keeping tunnel processes alive in side terminals, and doing it all outside whatever client actually needs the connection.

For the specific cases, these guides go deep:

The way I handle all of this now is to stop running the tunnels out-of-band. In ShellYard, the SSH sessions, HTTP client, database inspector, and remote-desktop sessions share one workspace, so a connection you already have open becomes the route for everything else — no separate -L mappings, no proxy config per tool, no processes to babysit. It's free, runs locally, and needs no account. Download it here.