You have a private server in a VPC that’s only reachable through a bastion host. The “obvious” way to SSH there is the wrong way: copy your private key onto the bastion, then SSH from bastion to the private box. Now your private key sits on a multi-tenant jump server you don’t fully trust. If the bastion is ever compromised, the attacker has your key.
The right way is ProxyJump. Your client opens an SSH connection to the bastion, the bastion forwards a TCP tunnel to the private host, your client opens a second SSH connection over that tunnel — and at no point does your private key leave your laptop. It’s been in OpenSSH since 7.3 (2016), and it’s a one-flag change from how you’re already using SSH today.
The flag form
ssh -J you@bastion.example.com you@private.internalRead it left to right: jump through bastion.example.com to reach private.internal. Both connections use your local SSH config, your local keys, your local agent. The bastion never sees your key — only the encrypted SSH traffic going to the private host.
Multi-hop works the same way:
ssh -J you@bastion-1,you@bastion-2 you@private.internalTwo hops, comma-separated. Three is fine too. Latency adds up, but the auth model stays the same — your laptop is the only place that ever holds the key.
The ~/.ssh/config form
Typing -J every time gets old. Move it to your config:
Host bastion
HostName bastion.example.com
User you
IdentityFile ~/.ssh/id_ed25519
Host private
HostName private.internal
User you
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519Now ssh private just works. Tab-complete works. scp file private:/tmp/ works. rsync works. Anything that uses your SSH config — and that’s basically everything — picks up the ProxyJump automatically.
Why this is meaningfully better than copying keys to the bastion
- Your private key never touches the bastion’s filesystem. If someone roots the bastion, they get bastion-shell access to anything you reached through it while logged in (because they can hijack the running SSH session) — but they don’t get your offline key material to use later.
- Bastion compromise blast radius is bounded by session duration. Close the SSH session, the attacker’s leverage on the bastion against you ends. Compare to a leaked private key: that’s exfiltratable forever.
- You can use a hardware key (YubiKey, Secretive, Touch ID). The auth signature happens on your laptop. The bastion can’t possibly access a TPM or Secure Enclave that lives on a machine in your apartment.
- Audit logs are clearer. Each hop’s
auth.logshows exactly which user authenticated. With agent forwarding alternatives, the trail is messier.
The wrong alternative: agent forwarding
You’ll see suggestions to use ssh -A (“agent forwarding”) instead. Don’t. Agent forwarding lets the bastion talk back to your local SSH agent while you’re connected and ask it to sign auth challenges. Functionally similar, with a critical difference: a compromised bastion can sign any auth challenge for as long as you’re connected, including for SSH targets you didn’t intend to reach.
ProxyJump doesn’t have this attack surface — the bastion is just a TCP tunnel. It can’t see or use your agent.
Common gotchas
- The bastion needs to allow TCP forwarding. If
/etc/ssh/sshd_configon the bastion hasAllowTcpForwarding no, ProxyJump fails. Most defaults areyes; some hardened setups disable it. Either re-enable, or you can’t use this pattern. - The bastion must resolve the inner hostname. The TCP connection to
private.internaloriginates from the bastion. If the bastion’s DNS doesn’t know that name, you’ll get “Could not resolve hostname.” Fix: use the inner host’s IP, or set up the bastion’s/etc/hostsappropriately. - Different usernames per hop. If your bastion username is
jumpuserand your inner-host username isdeploy, both end of-Jneed their own user prefixes:ssh -J jumpuser@bastion deploy@inner. - Old OpenSSH on the bastion. Pre-7.3, the only way to do this was
ProxyCommand ssh -W %h:%p bastionin your config. It still works (it’s literally what-Jcompiles to internally), and you’ll see it in older docs. If you can use the modern flag, do.
