Use ProxyJump, Bastion Hosts, and Avoid Agent Forwarding
A bastion (jump host) is an intentional boundary between an administrator and private servers. OpenSSH ProxyJump asks the client to connect to the bastion and then carry a separate SSH connection to the final host through it. The bastion does not need the final host’s private key, and the administrator does not need to expose the final host to a public network. This is different from logging into the bastion and running a second, manually composed SSH command.
Design the bastion boundary
Place the bastion on a private administration segment or a separately protected access service. Permit it to reach only the required final hosts and TCP/22, not every internal subnet. Permit final hosts to accept SSH from the bastion’s approved address or interface. Keep the bastion patched, monitored, time-synchronized, and free of application workloads that do not belong in the access boundary.
Do not create a WAN port forward to a final host. If the site needs remote administration, use its approved private access path to the bastion and enforce identity, MFA, source policy, and host-key verification there. A bastion is a control point, not a reason to make the whole private network routable.
Configure a named jump host
# ~/.ssh/config
Host bastion-admin
HostName 192.168.50.20
User jumpadmin
IdentityFile ~/.ssh/id_ed25519_jump
IdentitiesOnly yes
ForwardAgent no
Host app-private
HostName 192.168.60.10
User admin
IdentityFile ~/.ssh/id_ed25519_admin
IdentitiesOnly yes
ProxyJump bastion-admin
ForwardAgent no
ExitOnForwardFailure yes
The first connection verifies the bastion’s host key; the final connection separately verifies the private host’s host key. Because the final SSH handshake is still made by the client through the byte stream, the client’s known_hosts remains the place where the final host identity is checked. Do not replace verification with StrictHostKeyChecking=no.
# Inspect both policies without opening a connection.
ssh -G bastion-admin | grep -E '^(hostname|user|port|identityfile|identitiesonly|forwardagent) '
ssh -G app-private | grep -E '^(hostname|user|proxyjump|identityfile|identitiesonly|forwardagent) '
# Connect through the configured jump.
ssh app-private
Use ssh -J bastion-admin app-private for a deliberate one-off path, but prefer a reviewed alias for repeatable administration. Keep the bastion and destination identities distinct where possible. A final host that needs only a deployment key should not trust the bastion’s administrator key.
Understand ProxyJump versus agent forwarding
ProxyJump forwards the connection stream. It does not copy a key to the bastion and does not require ssh-agent forwarding. The client authenticates to the bastion and separately authenticates to the final host using the final host’s configured identity. This is the safer default.
# Confirm that forwarding remains off for the private target.
ssh -G app-private | grep -E '^(proxyjump|forwardagent|identityfile|identitiesonly) '
Agent forwarding (the -A option or ForwardAgent yes) exposes a socket to the remote session. The private key normally stays on the local machine, but a compromised bastion or account can ask the forwarded agent to sign authentication challenges while the socket is available. The attacker does not need to read the private key to impersonate its holder to other servers.
Keep ForwardAgent no globally and in bastion aliases. Do not turn it on because a command says an agent is required; first ask whether the final host can be reached by ProxyJump with a second local identity, a constrained deployment key, or an approved identity service. If a narrowly scoped exception is unavoidable, use a dedicated short-lived key or a confirmation-protected agent, restrict the bastion’s reachability, and remove the exception immediately after the change. Never use an agent that holds unrelated production keys for an untrusted jump host.
Use multiple jumps carefully
# Each hop is explicit and reviewed.
Host inner-app
HostName 10.20.30.40
User admin
IdentityFile ~/.ssh/id_ed25519_inner_app
IdentitiesOnly yes
ProxyJump bastion-admin,regional-jump
ForwardAgent no
Every intermediate host needs a separately verified host key and a network path to the next hop. Multiple jumps increase latency, failure modes, and audit scope. Do not hide an unapproved route in a wildcard ProxyJump rule. Test DNS and address resolution from the client and record which hop is allowed to reach which destination.
Restrict the bastion and final hosts
On the bastion, allow only named jump accounts and required destinations. On the final host, restrict SSH sources to the bastion address and keep root login and password authentication disabled after key access is tested. A server-side policy might include:
# On the final host, after checking the distribution's include order:
AllowUsers admin@192.168.50.20
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowTcpForwarding no
X11Forwarding no
These are examples, not a reason to paste unknown addresses into production. Use the final host’s actual bastion source address and account policy. Validate with sshd -t and inspect sshd -T before a reload; keep an active recovery session and a console path. A source restriction does not replace host-key verification or user authorization.
Diagnose the path without weakening it
# Verbose output is temporary and may reveal names and addresses.
ssh -vv app-private
# Test the bastion path, then the final path, separately.
ssh bastion-admin 'id'
ssh app-private 'id'
- If the bastion connection fails, check its host key, source policy, account, and route.
- If the final connection fails, check the final host key, destination account/key, bastion-to-final ACL, and the final host’s authentication logs.
- If host resolution is ambiguous, use explicit private addresses in a reviewed alias while the naming service is corrected.
- Do not use
ssh -A, copy a private key to the bastion, disable host checking, or broaden a firewall rule as a diagnostic shortcut.
Sequence navigation
Previous: Use Local, Remote, and Dynamic SSH Port Forwarding · Next: Use OpenSSH Certificates and Key Revocation
dispelled