ProxyJump and bastion hosts via ~/.ssh/config
Sometimes a server sits in a private network with no public IP. The only entry point is a bastion host with a public address. Typing ssh -J user@bastion user@private every time gets old fast. Here’s how to configure everything in ~/.ssh/config so you can reach private networks in one command.
Why you need a bastion host
A bastion (jump host, jump box) is an intermediate server with public access that proxies connections to infrastructure without external IPs. The typical topology:
The bastion doesn’t need to be hardened like a fortress — it’s just an open relay point. Access control lives on SSH keys and, if needed, security groups or firewall rules.
The bastion host is not a terminal destination — it only proxies traffic. You don’t need to run a VPN or additional services on it.
ProxyJump — the modern syntax
-J (ProxyJump) arrived in OpenSSH 7.3. The parameter takes a host in [user@]host[:port] format and spins up a SOCKS5 proxy through the specified node.
Basic invocation:
Authentication on both hosts via keys. If the username matches, you can omit it:
With a port other than 22:
The same thing in the config file:
After that, ssh private-server connects through the bastion automatically.
ProxyCommand — the classic approach
ProxyJump is a wrapper around ProxyCommand. When you need more control or are working with older OpenSSH, use ProxyCommand directly.
-W forwards stdin/stdout to the target host. In the config:
The difference from ProxyJump is minimal, but ProxyCommand lets you inject variables, conditions, and command chains.
Multiple hops in a row
A chain of two bastion hosts:
In the config:
OpenSSH connects hosts sequentially: laptop → bastion1 → bastion2 → private-server. Make sure your keys are present on each node.
For complex scenarios, ProxyCommand with nc (netcat) gives more flexibility:
The chain works, but each hop adds latency. For interactive work, more than two hops signals a network architecture problem.
Complete config example
ForwardAgent yes on the bastion lets your agent forward keys further down the chain. Don’t enable it if you don’t trust the bastion machine.
LocalForward in the example tunnels PostgreSQL from the private server to local localhost:5433. Useful for connecting IDEs or psql.
Verify the config parses without errors:
If you see the correct values — the config was picked up.
Common mistakes and solutions
Connection timeout during ProxyJump
Verify the bastion is reachable directly:
If that fails — the problem is in the network, not the config.
Permission denied (publickey) on bastion
Make sure the key is loaded in the ssh-agent:
If the agent is empty — add the key and test with ssh -vT bastion.
Works one way, not the other
ProxyJump tunnels TCP. ICMP (ping) won’t pass through. Check connectivity with nc -zv host port or ssh -v.
Agent refused operation during agent forwarding
Check the SSH_AUTH_SOCK variable:
If empty — start the agent:
Slow connection through a chain
Check MTU. Sometimes MTU in VPN/LAN is smaller than required for TCP-over-TCP. Add to the config:
For very slow links, try compression:
ProxyJump covers 90% of use cases. If you need visualization or a UI manager — look at ssh-config tools or Terminator, but for console work ~/.ssh/config with ProxyJump is sufficient.