# ProxyJump and bastion hosts via ~/.ssh/config

LLMS index: [llms.txt](/en/llms.txt)

---

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:

```bash
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:

```bash
ssh -J bastion.example.com 10.0.1.5
```

With a port other than 22:

```bash
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.

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
echo $SSH_AUTH_SOCK
```

If empty — start the agent:

```bash
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.
