ProxyJump vs ProxyCommand for SSH Bastions
Prefer ProxyJump for nested SSH hops, and ProxyCommand when you need netcat, TLS wrappers, or a custom transport through a Lucknow bastion VPS.
Netbay Developer Relations
Netbay Engineering
On this page
ProxyJump and ProxyCommand both get you through a bastion. They are not interchangeable, and treating them as synonyms is how people end up with a netcat one-liner that breaks the moment they add a second hop. ProxyJump is an OpenSSH-native hop: the client opens a direct-tcpip channel through an already authenticated SSH session and runs a second SSH protocol on that channel. ProxyCommand is a stdin/stdout pipe to any program you name. Use the first until you cannot. Keep the second for transports SSH does not speak by itself.
This is the next layer after a working ~/.ssh/config. You already have Host aliases. The question is how those aliases reach a private node that has no public address.
ProxyJump is the hop you want
The -J flag and the ProxyJump keyword are the same feature. The client authenticates to the jump host using that host's own stanza (User, IdentityFile, Port), then asks the jump sshd to open a TCP channel to the next HostName:Port. A second SSH handshake runs on that channel, using the next stanza's key. Nested lists work: ProxyJump bastion,mid,inner. Evaluation is left to right. Each hop can have its own key and user.
Host bastion
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519_bastion
IdentitiesOnly yes
Host mid
HostName 10.0.0.8
User deploy
IdentityFile ~/.ssh/id_ed25519_mid
IdentitiesOnly yes
ProxyJump bastion
Host app
HostName 10.0.1.14
User deploy
IdentityFile ~/.ssh/id_ed25519_app
IdentitiesOnly yes
ProxyJump midNotice mid already jumps through bastion, so app does not list both. You can also write ProxyJump bastion,mid on app and skip the indirection. Pick one style and keep it. Mixing both in the same file makes hop order impossible to review.
On the server, the jump host must allow TCP forwarding. AllowTcpForwarding yes is the default on stock OpenSSH. Disable it only if the box is not a bastion. PermitOpen can restrict which destinations the jump will connect to, which is the right hardening once you know the private CIDR of the app nodes. A Lucknow VPS used as a bastion should permit only that CIDR, not the whole internet through your key.
ProxyCommand is a pipe, not a hop
ProxyCommand runs a local process and treats its stdin and stdout as the encrypted stream. The classic form is ssh -W %h:%p bastion, which is almost ProxyJump implemented in userland. %h and %p expand to the target HostName and Port. %n is the original alias. %r is the remote user. Those tokens are the entire contract. Quote them so a hostile hostname cannot inject a shell.
Host app-via-nc
HostName 10.0.1.14
User deploy
IdentityFile ~/.ssh/id_ed25519_app
IdentitiesOnly yes
ProxyCommand ssh -W %h:%p bastion
Host app-via-ncat
HostName 10.0.1.14
User deploy
IdentitiesOnly yes
IdentityFile ~/.ssh/id_ed25519_app
ProxyCommand ncat --proxy 127.0.0.1:9050 --proxy-type socks5 %h %pYou need ProxyCommand when the path is not SSH. Examples: corkscrew through an HTTP CONNECT proxy, ncat through a SOCKS proxy, a vendor wrapper that speaks TLS to a broker, or a tiny socat unit that presents a serial-like socket. None of those are nested SSH hops. ProxyJump cannot call corkscrew. ProxyCommand can.
The cost is that OpenSSH no longer understands the hop graph. Multiplexing, jump chaining, and some ControlMaster behaviors get harder. Error messages become whatever the child process printed on stderr. If ncat is missing, you get a broken pipe, not a clean "could not resolve bastion".
When the extra hop is not SSH
A few transports still show up on real fleets. A corporate HTTP proxy that only allows CONNECT to port 443: run sshd on 443 on the bastion, then ProxyCommand corkscrew proxy.example 8080 %h %p. A SOCKS forward you already opened for a browser: ncat --proxy. A jump through a machine that only offers a forced command: you may have to ProxyCommand ssh bastion -W %h:%p anyway, because ProxyJump can fail when the remote account cannot open direct-tcpip.
Test with ssh -v app. The debug log states whether the client used a jump or spawned a command. Timeouts of 60 seconds with no banner usually mean the pipe never connected, not that the key is wrong. Authentication errors after a banner mean the hop worked and the second handshake failed.
Keep both mechanisms out of login shells. A ProxyCommand that calls a script in ~/bin is fine if that script is mode 700 and does not source a noisy profile. A ProxyCommand that calls a script which itself calls ssh without -o BatchMode=yes can deadlock when two password prompts fight for the tty.
Debugging hops without guessing
ssh -G app still prints the expanded ProxyJump or ProxyCommand. ssh -vvv app true then shows the channel setup. On the bastion, journalctl -u ssh -n 50 shows whether the direct-tcpip request was accepted or denied by PermitOpen. If you tightened PermitOpen to 10.0.1.14:22 and the app moved, the jump fails closed. That is the correct failure mode.
Do not enable GatewayPorts on the bastion to "make jumps work". GatewayPorts controls bind addresses for remote forwards, not ProxyJump. Confusing the two is a common copy-paste from tunnel guides.
A short takeaway: ProxyJump is nested SSH and should be the default for every private node behind a Lucknow bastion. ProxyCommand is the escape hatch for non-SSH transports. Name the hop in config, restrict PermitOpen to the private CIDR, and leave netcat for the cases SSH cannot express. Spin up a pair of Ubuntu 24.04 instances on Netbay in under 60 seconds and walk the hop with ssh -v — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance