Developer & Server Workflows

Get Files Off a Remote Server Without Opening Inbound Ports

By SRIFT maintainers · Published

Reviewed by SRIFT maintainers · last reviewed against the SRIFT 4.3.2 source · third-party facts checked 8 October 2026.

Short answer

When a server accepts no inbound connections, reverse the direction: make the server open the connection. The practical options are SSH through a jump host (ssh -J) or a reverse tunnel, your cloud provider's agent-based tunnel (Google IAP, AWS Systems Manager), uploading to object storage and sending a presigned URL, or a streamed link: the server holds an outbound HTTPS connection to a relay and serves the bytes only when someone opens the link.

Which one fits depends on what the network already allows. If you have SSH via a bastion, use it. If the machine can only reach the internet on outbound port 443, the streamed link is often the only option that needs no new firewall rule, credentials or stored copy.

Inbound SSH available?
Use scp, sftp or rsync, via ssh -J if there is a bastion
Only outbound 443?
Presigned storage URL or an outbound streamed link
Avoid
Exposing python3 -m http.server on a public interface

Why "just scp it" often fails

Copying a file off a machine is trivial when you can reach port 22 on it. In practice the machines that hold the interesting files (a production database dump, a crash core, last night's logs) are the ones you deliberately made hard to reach. Four situations come up repeatedly:

  • A production VM behind a bastion. SSH is allowed, but only from one hardened jump host. Your laptop cannot connect directly.
  • A cloud VM with no public IP. It sits in a private subnet and reaches the internet through a NAT gateway. Nothing on the internet can open a connection to it.
  • A headless box behind NAT. A Raspberry Pi in a lab, a machine on a home or carrier-grade NAT connection. It has no stable public address and you do not control the router.
  • A locked-down corporate server. Egress goes through an HTTP proxy, only port 443 is allowed out, and opening any inbound port requires a change request.

What all four share is that outbound connections still work, at least on port 443. Every method below is either a way to use an inbound path that already exists or a way to turn an outbound connection into a delivery channel.

SSH-based options: jump hosts and reverse tunnels

Through a bastion with -J

OpenSSH's -J option (the ProxyJump directive) connects to the jump host first and forwards a TCP stream from there to the target, so the session stays end-to-end encrypted between your laptop and the target; the bastion only relays it. scp and sftp accept -J directly, several hops can be chained with commas, and rsync takes it through -e:

Pull through a jump host
scp -J [email protected] [email protected]:/var/backups/db.sql.gz .
sftp -J [email protected] [email protected]
rsync -avz --partial -e "ssh -J [email protected]" \
  [email protected]:/var/log/app/ ./app-logs/

Since OpenSSH 9.0, scp uses the SFTP protocol by default; -O selects the legacy SCP protocol for servers without an SFTP subsystem. For large or repeated pulls, rsync is the better tool because it resumes partial files and sends only changed blocks.

Reverse tunnels

If the server can SSH out to a host you control, it can publish its own SSH port there with -R. By default the forwarded port listens only on the remote host's loopback interface (a public bind needs GatewayPorts on that host), which is what you want: you then reach it through that host as a jump.

On the server, then on your laptop
# server: expose its sshd as port 2222 on relay-host's loopback
ssh -N -R 2222:localhost:22 [email protected]

# laptop: "localhost" is resolved on relay-host, the jump host
scp -J [email protected] -P 2222 ops@localhost:/var/log/app.log .

This works from behind NAT, but it needs a reachable SSH host of your own, a key for the server to log in with, and a policy that lets the server hold a long-lived SSH session out. Many locked-down networks block outbound port 22 entirely, and a persistent tunnel into production is a standing access path that security teams rightly ask about.

Cloud tunnels, presigned URLs and ad-hoc web servers

The cloud provider's tunnel

Cloud providers offer SSH paths that need no public IP. On Google Cloud, gcloud compute scp --tunnel-through-iap carries the SSH session through Identity-Aware Proxy TCP forwarding; the VM needs no external address, but a firewall rule must allow ingress on port 22 from IAP's range 35.235.240.0/20, and access is governed by IAM. On AWS, Systems Manager Session Manager can tunnel SSH (and therefore scp) through the SSM Agent, which connects outbound to AWS endpoints, so no inbound port or bastion is needed. You still need sshd and a key on the instance, the Session Manager plugin locally, and IAM permission to start the session. AWS notes that session logging is not available for SSH or port-forwarding sessions, because Session Manager only sees the encrypted tunnel.

Examples
gcloud compute scp --tunnel-through-iap --zone=us-central1-a \
  app-vm:/var/backups/db.sql.gz .

# ~/.ssh/config entry from the AWS docs, then plain scp works
Host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
scp -i key.pem ec2-user@i-0123456789abcdef0:/var/log/app.log .

These are the right answer when the machine is already enrolled and you already have the IAM roles. They do not help with a box outside that cloud, or when the person who needs the file has no cloud account.

Object storage and presigned URLs

Upload the file to a bucket, then generate a presigned URL that anyone can use without credentials. It is simple and works from any network that reaches the storage endpoint, but it has three costs. The server needs write credentials to the bucket. The file now exists as a stored copy that someone must delete (set a lifecycle rule). And the URL is a bearer token: AWS describes presigned URLs exactly that way, they work any number of times until they expire, and with Signature Version 4 they last at most 7 days when signed with IAM user keys. Signed with temporary credentials, such as an EC2 instance role, a URL dies when those credentials expire, whatever expiry you asked for.

python3 -m http.server

The quickest hack is to start Python's built-in web server in the directory and download from it. Python's own documentation says the module is not recommended for production and implements only basic security checks. By default it listens on port 8000 on all interfaces, has no authentication, lists directories and follows symbolic links, so files outside the directory can be served. It also needs an inbound port, which is the thing you do not have.

The options side by side

What each method requires from the server's network and what it leaves behind.
MethodInbound port on serverCredentials on serverStored copyEncryption
scp / sftp / rsync (direct)Yes, 22None beyond sshdNoSSH, end to end
Same, via ssh -J22 from the bastion onlyNone beyond sshdNoSSH, end to end (bastion relays)
Reverse tunnel (ssh -R)NoKey to log in to your relay hostNoSSH
gcloud IAP / AWS SSMIAP: 22 from Google's range; SSM: noneCloud agent and IAM rolesNoSSH inside the provider's TLS tunnel
Bucket + presigned URLNoBucket write credentialsYes, until deletedTLS; at rest per bucket settings
python3 -m http.serverYes, 8000 by defaultNoneNoNone (plain HTTP)
Streamed link (srift quick-share)No (outbound 443 only)NoneNoTLS; end to end with --encrypt

For the wider tool landscape, including Magic Wormhole and croc, see the command-line transfer tools comparison. If proxies and firewalls are the main obstacle, the guide to corporate firewalls and proxies covers what typically gets blocked.

Security notes before you share anything

  • A link is a bearer secret. Presigned URLs and streamed links alike grant access to whoever holds them. Send them over a channel you would trust with the file, and use single-use or short-lived links for anything sensitive.
  • Without --encrypt, the relay handles plaintext. A SRIFT link created without --encrypt is protected by TLS in transit only: the relay sees the real file name and the bytes as they pass through. With --encrypt, the file is sealed with AES-256-GCM in 1 MiB chunks on the server, the key travels in the #k= fragment (which browsers do not send to servers), and the relay sees only ciphertext, its size and the placeholder name encrypted.srift. Read what a relay can see for the full picture.
  • Browser decryption trusts the page. When the recipient opens an encrypted link in a browser, the decryption code is served by srift.app. A compromised server could serve code that reads the key. srift get decrypts with the locally installed CLI instead. --password adds a second secret that you send separately.
  • Metadata remains. The relay sees both sides' IP addresses, the timing and the transfer size, whichever mode you use.
  • Local history holds keys. The CLI records the links it created, including keys, in ~/.srift/history.jsonl (mode 0600, 30 days by default). On a shared server, disable it with SRIFT_NO_HISTORY=1 or clear it with srift history --clear.
  • Check policy first. Exporting a production dump through any channel may be governed by data handling rules. A method that works around the firewall does not work around those.

Frequently asked questions

Can I download a file from a server that has no public IP address?

Yes, as long as the server can make outbound connections. Use SSH through a host that can reach it (ssh -J), your cloud provider's tunnel such as IAP or Systems Manager, upload to object storage and share a presigned URL, or have the server serve a streamed link through an outbound connection to a relay.

Does a streamed link keep working after I log out of the server?

With SRIFT, the link is served by a background daemon that keeps running after the command and your SSH session end. In embedded mode, used where a daemon cannot start, the command itself serves the link, so run it under tmux, screen or nohup, or use --wait in a script.

Is rsync better than scp for pulling large files?

Usually. With --partial, rsync keeps a partially transferred file and continues from it on the next run, and on repeated pulls it sends only the changed parts of files. scp starts over after an interruption.

References

  1. ssh(1) manual: -J and -R options (OpenBSD / OpenSSH)
  2. scp(1) manual (OpenBSD / OpenSSH)
  3. Using IAP for TCP forwarding (Google Cloud)
  4. Allow and control permissions for SSH connections through Session Manager (AWS)
  5. Download and upload objects with presigned URLs (AWS)
  6. http.server: HTTP servers (Python Software Foundation)

Use SRIFT from a terminal

One command prints a download link that streams from your machine. Needs only outbound HTTPS; --encrypt keeps the key out of the relay.

npx -y srift-transfer quick-share ./build.tar.gz --encrypt