How-to

Connecting through an SSH bastion from a Mac

4 min read

Plenty of production servers are not reachable from the internet at all. They sit on a private network, and the only door is a hardened machine that does nothing else: the bastion, or jump host. You connect to the bastion, and from there to the server you actually want.

On the command line this is a solved problem — one line of ssh config and you forget about it. In a graphical file transfer client it usually is not, which is why people behind a bastion end up doing everything over the terminal even when they would rather drag a folder.

What ProxyJump actually does

ProxyJump tells SSH: before talking to the target, open a connection to this other host, and tunnel through it. SSH opens a session to the bastion, asks it to forward a TCP connection to the target’s port 22, and then runs a completely normal SSH handshake with the target inside that tunnel.

The important consequence is that the bastion never sees your traffic in the clear. It forwards encrypted bytes. Your authentication with the target, and the target’s host key, are negotiated end to end — the bastion is a pipe, not a man in the middle.

The ssh config side

In ~/.ssh/config you describe the bastion as a normal host, then point the target at it:

Host bastion
    HostName bastion.example.com
    User jump
    IdentityFile ~/.ssh/id_ed25519

Host app-prod
    HostName 10.0.4.21
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump bastion

You may also meet the older form, still very common: ProxyCommand ssh -W %h:%p bastion. It does the same job; %h and %p are the target host and port.

Test it from the terminal before involving any other tool:

ssh app-prod

If that works, every SSH-based tool on your Mac can be made to work. If it does not, no GUI client will rescue you — fix the config first.

Chains, and why the order matters

ProxyJump accepts more than one hop. ProxyJump edge,inner means go through edge first, then inner, then the target. Each hop is opened inside the previous one, so the chain is built outwards from your Mac.

This also means each hop authenticates separately. If the first bastion accepts your key but the second one does not, the failure happens halfway — worth knowing, because the error you see will be about the second hop, not the first.

The part people get wrong: host keys

Every hop has its own host key, and every hop must be verified against its own known-hosts entry. It is tempting for a client to verify only the final target — after all, that is where your files go — but that is a false sense of safety: a compromised bastion could then present itself as the target and read everything.

If a tool asks you to trust a fingerprint when connecting through a bastion, check which host it is talking about. It should name the hop it is currently opening, not always the destination. This is the same trust-on-first-use model described in certificates and host keys.

Doing it from a Mac file client

Upstream (FULL) reads ProxyJump straight from your ssh config: import ~/.ssh/config and the bastion is created as its own saved site and linked to the target automatically. You can also set it by hand — open the site, pick the jump host under “Connect through”, and connect as usual.

Each hop is verified against its own known-hosts entry, and connection errors say which hop failed: “jump host unreachable” and “target unreachable through the bastion” are different problems with different fixes.

Step by step

  1. Describe the bastion in ~/.ssh/config — add a Host block for the bastion with its HostName, User and IdentityFile, exactly as if you were going to connect to it directly.
  2. Point the target at it with ProxyJump — in the target’s Host block, add ProxyJump <bastion-alias>. Test it from the terminal with ssh <target-alias> before involving any other tool.
  3. Import the config into your client — in Upstream, use Import ▸ From ~/.ssh/config. Both hosts are imported and the jump relationship is preserved.
  4. Authorise the private key once — macOS sandboxing means an app cannot read ~/.ssh on its own. On the first connection Upstream asks you to select the key the config points to; it is then stored in your Keychain.
  5. Connect — connect to the target as usual. Upstream opens the bastion first, then tunnels to the target, verifying each host key along the way.

Questions people ask

Is a bastion the same as a VPN?

No. A VPN puts your whole machine on the remote network; a bastion forwards a single connection to a single host. A bastion is narrower, which is exactly why security teams prefer it for server access.

Does the bastion see my files?

No. It forwards encrypted traffic. Your session with the target is negotiated end to end, inside the tunnel.

Can I chain more than one bastion?

Yes. ProxyJump accepts a comma-separated list and each hop is opened inside the previous one. Upstream follows chains of any length.

Does this work with plain FTP?

No, and that is not a limitation of the client. ProxyJump is an SSH feature, so it applies to SFTP. FTP and FTPS have no equivalent.

Upstream — the native FTP, FTPS & SFTP client for macOS

A transfer queue you can pause, resume and reorder, folder synchronization with a dry-run preview, live remote editing and credentials in the macOS Keychain. Free to download, no subscription.