How-to

Moving Files Through an SSH Bastion

7 min read

Getting a shell through a jump host is a solved problem: one ProxyJump line in ~/.ssh/config and you stop thinking about it. Getting files through one is a different question, and the difference catches people out — because it depends on which protocol you are carrying.

This guide is about that difference. If what you need is the ssh config side — writing the Host blocks, chaining hops, debugging the tunnel itself — that is covered properly in connecting through an SSH bastion on our SSH client’s site, and it is worth reading first. Everything below assumes ssh target already works from your Terminal.

Why file transfer is a separate question

A bastion forwards one TCP connection to one host and port. That is the whole model, and it is why security teams like it: narrow, auditable, nothing else gets through.

An interactive SSH session fits that model exactly — one connection, everything multiplexed inside it. Whether a file transfer fits depends entirely on whether your protocol also lives inside a single connection.

SFTP crosses a bastion cleanly

SFTP is not a separate protocol in the network sense. It is a subsystem running inside an SSH session — the same connection that would carry your shell carries the file data instead, as another channel.

So everything true of ssh target is true of SFTP to that target. One tunnel, one host key exchange with the target, end-to-end encryption that the bastion cannot read. Chains work, keys work, and there is nothing extra to configure beyond the jump host itself.

If your bastion gives you SSH access to the target, you already have working SFTP to it, whether or not anyone told you so.

FTP and FTPS cannot — and it is not the client’s fault

Plain FTP and FTPS use two connections, not one. There is a control connection on port 21 where commands and replies travel, and a separate data connection opened for every directory listing and every file. That second connection is the problem: the tunnel carries port 21 and nothing else.

What you see in practice is distinctive. You forward the control port, you log in successfully, and then the first LIST hangs forever. Authentication worked because it happens on the control connection. The listing did not, because it needs a connection the tunnel never carried.

It gets worse in passive mode, which is what almost everything uses today. The server answers PASV by telling the client which address and port to connect to — and behind a bastion it will hand you an address on the private network, something like 10.0.4.21, which your Mac cannot reach at all. Active vs passive FTP explains the mechanics of that exchange in full.

FTPS adds a third obstacle: many servers require the data connection to resume the TLS session negotiated on the control connection, so even a correctly forwarded data port can be refused.

None of this is something a client can fix. It is the protocol.

What to do when the host only offers FTP

In rough order of how well they work:

Check whether SFTP is available anyway. This is the answer far more often than people expect. If the bastion authenticates you over SSH, the target usually runs an SSH daemon too — and if it does, SFTP is already there. Try it before engineering anything.

Forward the control port and the passive range. Technically possible, genuinely fragile. It requires the server to be configured with a narrow, fixed passive port range and to advertise a reachable address (pasv_address in vsftpd, MasqueradeAddress in ProFTPD), then a -L forward for port 21 plus one for every port in that range. If you do not control the server’s configuration, you cannot make this work.

Move the transfer to the bastion. Copy files to the jump host with SFTP, then push them onward from there. Two hops by hand, but it always works and needs nothing configured.

The bastion becomes your bottleneck

Worth setting expectations before you move a large directory. Every hop adds a full round trip to the latency, and jump hosts are typically sized for shell sessions — a small instance whose job is to accept SSH and forward it, not to saturate a gigabit link with file data.

The practical consequences: throughput through a bastion is often well below what you get connecting directly, raising the concurrent-transfer count helps less than usual because every stream shares the same hop, and a large transfer is more likely to be interrupted simply because there are more moving parts between you and the file. Resumable transfers stop being a nicety and start being the reason the job finishes.

Doing it from a Mac file client

In Upstream (FULL), a jump host is a property of the site rather than a separate mode. Import ~/.ssh/config and any target using ProxyJump (or the older ProxyCommand ssh -W) arrives with the bastion already created as its own site and linked as the jump host. To set one by hand, open the site, pick the bastion under “Connect through”, and connect normally.

Two details that matter for transfers specifically. Each hop is verified against its own known-hosts entry, so a compromised bastion cannot present itself as the target — see certificates and host keys for the trust model. And the transfer queue treats a dropped tunnel like any other failure: it retries with backoff and resumes the partial file rather than restarting it, which is what makes a large transfer over a fragile chain survivable.

Step by step

  1. Get the tunnel working in the Terminal firstssh target must succeed before any GUI client is involved. If it does not, fix ~/.ssh/config; no application can work around a broken jump.
  2. Confirm the target actually speaks SFTPsftp target from the same Terminal. If this works, you are done deciding: use SFTP and ignore everything about FTP below.
  3. If only FTP is on offer, stop and ask why — check with whoever runs the server whether SSH is available on the target. It usually is, and it removes the problem entirely.
  4. Import ~/.ssh/config into your client so the jump relationship comes across instead of being retyped, along with each host’s IdentityFile path.
  5. Authorise the private key once — a sandboxed Mac app cannot read ~/.ssh unprompted, so you select the key at first connection and it is kept in the Keychain afterwards.
  6. Test with a small file both ways before moving anything large, to confirm the whole chain carries data and not just the login.

Questions people ask

Does SFTP work through a jump host without extra configuration?

Yes. SFTP runs inside the SSH session, so a working ProxyJump carries it with no additional setup. If ssh target works, SFTP to that target works.

Why does FTP log in through a tunnel and then freeze?

Because logging in only needs the control connection, which your tunnel carries, while listing a directory needs a second data connection, which it does not. The freeze at the first listing is the signature of exactly this problem.

Can I use plain FTP through a bastion at all?

Only if you control the server’s configuration and can pin a narrow passive port range with a routable advertised address, then forward every one of those ports. If you cannot change the server, the answer is no.

Is the transfer slower through a bastion?

Usually yes. Each hop adds round-trip latency, and jump hosts are commonly small instances provisioned for shell access rather than bulk data.

Does the bastion see my files?

No. It forwards encrypted bytes. Your session is negotiated end to end with the target, inside the tunnel, and the jump host has no key material for it.

Can I chain several bastions for a transfer?

Yes. ProxyJump accepts a comma-separated list and each hop opens inside the previous one. Latency accumulates with every hop, so keep chains as short as the network actually requires.

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.