Skip to content

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:

Laptop → Bastion (public IP) → Private server (10.0.1.5)

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.

Note

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:

ssh -J user@bastion.example.com user@10.0.1.5

Authentication on both hosts via keys. If the username matches, you can omit it:

ssh -J bastion.example.com 10.0.1.5

With a port other than 22:

ssh -J bastion.example.com:2222 10.0.1.5

The same thing in the config file:

Host private-server
    HostName 10.0.1.5
    ProxyJump bastion.example.com

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.

ssh -o ProxyCommand="ssh -W %h:%p bastion.example.com" 10.0.1.5

-W forwards stdin/stdout to the target host. In the config:

Host private-server
    HostName 10.0.1.5
    ProxyCommand ssh -W %h:%p bastion.example.com

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:

ssh -J bastion1.example.com,bastion2.example.com 10.0.1.5

In the config:

Host private-server
    HostName 10.0.1.5
    ProxyJump bastion1.example.com,bastion2.example.com

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:

Host dmz-server
    HostName 192.168.1.10
    ProxyCommand ssh -W %h:%p bastion.example.com

Host private-server
    HostName 10.0.1.5
    ProxyCommand ssh -W %h:%p dmz-server

The chain works, but each hop adds latency. For interactive work, more than two hops signals a network architecture problem.

Complete config example

# Bastion host (public entry point)
Host bastion
    HostName bastion.example.com
    User admin
    Port 22
    IdentityFile ~/.ssh/id_ed25519
    ForwardAgent yes
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Private server via bastion
Host private-web
    HostName 10.0.1.5
    User appuser
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Private database
Host private-db
    HostName 10.0.2.10
    User dbadmin
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 5433 127.0.0.1:5432
Tip

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:

ssh -G private-web | grep -E '^(hostname|proxyjump)'

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:

ssh bastion.example.com echo ok

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:

ssh-add ~/.ssh/id_ed25519
ssh-add -l

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:

echo $SSH_AUTH_SOCK

If empty — start the agent:

eval "$(ssh-agent -s)"
ssh-add

Slow connection through a chain

Check MTU. Sometimes MTU in VPN/LAN is smaller than required for TCP-over-TCP. Add to the config:

Host *
    IPQoS lowdelay throughput

For very slow links, try compression:

Host *
    Compression yes

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.