SFTP through a jump host: reaching servers behind a bastion
To use SFTP through a jump host, you connect to the bastion over SSH and ask it to open a forwarded connection to the internal server, then run your SFTP session through that connection. With OpenSSH it is a single option: sftp -o ProxyJump=user@bastion user@internal-host. Your files are encrypted end to end between your Mac and the internal server. The bastion only passes encrypted bytes along, and it never needs your key for the internal machine.
This post covers the command-line setup, what the bastion has to allow, and how host keys and logins work for each hop.
Why bastions exist
Servers in a private subnet (databases, application servers, build machines) usually have no public address at all. A single hardened host, the bastion or jump host, sits at the edge with SSH open to the internet or to a VPN, and everything else is reachable only from it. That gives you one place to apply firewall rules, two-factor login and auditing, instead of exposing every machine.
SFTP through a jump host from Terminal
The ProxyJump option, added in OpenSSH 7.3, does all the work:
sftp -o ProxyJump=alice@bastion.example.com deploy@10.0.2.15
OpenSSH connects to the bastion as alice, asks it to open a TCP connection to 10.0.2.15 on port 22, and then runs a complete second SSH session, as deploy, inside that forwarded connection.
For anything you use more than once, put it in ~/.ssh/config:
Host bastion
HostName bastion.example.com
User alice
IdentityFile ~/.ssh/id_ed25519
Host app1
HostName 10.0.2.15
User deploy
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
Now sftp app1 and ssh app1 both go through the bastion automatically. Several hops are written as a comma-separated list, such as ProxyJump bastion,inner-bastion. On older OpenSSH versions the equivalent is ProxyCommand ssh -W %h:%p bastion. The ssh_config manual documents both.
The target name is resolved by the bastion
The bastion makes the onward connection, so the target's hostname is looked up from the bastion's point of view. Private DNS names such as app1.internal, or private IP addresses, work fine even though your Mac could never resolve or route to them.
What the bastion has to allow
The forwarding is an SSH direct-tcpip channel, which the bastion's sshd must permit:
AllowTcpForwardingmust beyesorlocalfor your account (it defaults toyes). A bastion locked down withAllowTcpForwarding norefuses the channel, and you see a "channel open failed" or "administratively prohibited" error.PermitOpencan restrict which hosts and ports may be reached, which is good practice: list the internal servers' port 22 and nothing else.- A
Matchblock can give jump-only accounts no shell at all, so the bastion is a relay and not a place to work.
The sshd_config manual describes each option.
Prefer ProxyJump to agent forwarding
An older pattern is to SSH into the bastion with ForwardAgent yes and then SSH onwards from there. That works, but while you are connected anyone with root on the bastion can use your forwarded agent to log in as you anywhere your keys work. With ProxyJump the second session is negotiated from your Mac, so your keys and agent never touch the bastion.
Host keys and logins, hop by hop
Each hop is a separate SSH session, which means two of everything:
- Two host keys to verify. The first time, you are asked about the bastion's key and then the internal server's. Check both; the internal one is just as interceptable if the bastion is compromised. How to verify an SSH fingerprint covers how.
- Two logins. The bastion and the target may use different usernames, different keys, or different methods. A bastion that asks for a one-time code is common, and it asks each time a new session goes through it.
On AWS, an alternative to a classic bastion is Systems Manager Session Manager, which can forward SSH without an open inbound port. That is a different setup with its own agent and IAM permissions, outside the scope of this post.
Setting up a jump host in Wharf
In Wharf the bastion is simply another saved site. Set it up like any SFTP server, then point the internal server at it:
- Add a site for the bastion: SFTP, its public hostname, and the username and authentication method it expects.
- Add a site for the internal server. Use the address the bastion can reach, such as a private IP or internal DNS name, plus its own username and authentication.
- In the internal server's site, under Advanced, choose the bastion in the Jump host menu, and save.
When you connect, Wharf logs in to the bastion with the bastion site's credentials, opens a forwarded channel to the internal host, and runs the SFTP session through it. Each site authenticates with its own saved method, whether that is a password, an imported SSH key or a key plus a two-factor code, and each server's host key is verified separately the first time. If the bastion itself has a jump host set, Wharf chains through both. The tunnel runs inside the app, without opening a listening port on your Mac.
Two practical notes. First, jump hosts apply to SFTP sites. Second, each connection Wharf opens to the internal server gets its own tunnel, and Wharf uses several connections for parallel transfers and one for each open log window. If the bastion asks for a one-time code, or limits how many sessions you may open, lower Maximum connections in the internal site's Advanced section to reduce how often that happens.
If the bastion refuses to forward, the connection error says it failed via the jump host and includes the bastion's reason, which usually points straight at AllowTcpForwarding or PermitOpen. Everything else, from following logs to sync, works on a tunnelled site exactly as on a direct one. The features overview lists what else Wharf does, and the support page covers host key prompts.