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:
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.
# 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.
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.
Outbound-only streamed links
A streamed link inverts the usual download. The server runs a small client that connects out to a relay on port 443, registers the file and receives a URL. Nothing is uploaded at that point. When the recipient opens the URL, the relay asks the server for the bytes over the connection the server already holds, and forwards them into the recipient's HTTPS response. From the firewall's point of view, the server only ever made one outbound HTTPS connection.
The trade-off is availability: the server's process must be running when the recipient downloads, because there is no stored copy. Throughput is bounded by the server's upstream bandwidth and the relay path.
Doing it with SRIFT
SRIFT's quick-share command implements this pattern. Install the CLI with curl -fsSL https://srift.app/install.sh | sh (a prebuilt binary checked against a published SHA256SUMS file, including musl builds for Alpine) or run it with no install through npx -y srift-transfer on Node 20 or later.
# one file, end-to-end encrypted, single download, expires in 2 hours
srift quick-share /var/backups/db.sql.gz --encrypt --once --ttl 2h
# a folder becomes a .tar.gz (.git and node_modules are always excluded)
srift quick-share /var/log/app --encrypt --exclude "*.tmp"
# block until downloaded; exit code 3 if nobody downloads within 30 minutes
srift quick-share ./core.dump --encrypt --once --wait --wait-timeout 30m# encrypted links: browser, or srift get (decrypts locally, resumes)
srift get "https://srift.app/d/<token>#k=<key>"
# links made without --encrypt also work with plain tools
curl -fLOJ "https://srift.app/d/<token>"
wget --content-disposition "https://srift.app/d/<token>"Useful details from the implementation (version 4.3.2):
- Who serves the link. Normally the CLI starts a background daemon on
127.0.0.1:3822, and the link works while that daemon runs. On machines where a background process or local port is not allowed,quick-shareruns the daemon inside its own process (“embedded mode”) using only outbound HTTPS and WSS on port 443; the command then stays in the foreground until the download finishes. - No default expiry. Without
--ttl,--onceor--max-downloads, a link stays valid for as long as the daemon serves it. Set a limit, or revoke it withsrift links revoke <token>. - --wait-timeout never cuts a download short. The timeout applies only while no download is in progress, which makes it safe in scripts.
- Snapshots, not live files. A file that changes or disappears after the link is created is refused rather than served half-old, half-new, so copy a log that is still being written before sharing it. Input from stdin (
-) is buffered to a local temporary file first. - Proxies. The CLI honours
HTTPS_PROXY,HTTP_PROXY,ALL_PROXYandNO_PROXY, with HTTP CONNECT andsocks5://orsocks5h://proxies. Behind a TLS-inspecting proxy, pointNODE_EXTRA_CA_CERTSat the corporate CA.srift doctorprobes DNS, HTTPS, WebSocket, proxy and local-daemon paths and says which mode will work.
The same mechanism works inside containers and CI jobs; see moving files out of Docker and Kubernetes and sharing CI/CD build artifacts.
The options side by side
| Method | Inbound port on server | Credentials on server | Stored copy | Encryption |
|---|---|---|---|---|
| scp / sftp / rsync (direct) | Yes, 22 | None beyond sshd | No | SSH, end to end |
| Same, via ssh -J | 22 from the bastion only | None beyond sshd | No | SSH, end to end (bastion relays) |
| Reverse tunnel (ssh -R) | No | Key to log in to your relay host | No | SSH |
| gcloud IAP / AWS SSM | IAP: 22 from Google's range; SSM: none | Cloud agent and IAM roles | No | SSH inside the provider's TLS tunnel |
| Bucket + presigned URL | No | Bucket write credentials | Yes, until deleted | TLS; at rest per bucket settings |
| python3 -m http.server | Yes, 8000 by default | None | No | None (plain HTTP) |
| Streamed link (srift quick-share) | No (outbound 443 only) | None | No | TLS; 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
--encryptis 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 nameencrypted.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 getdecrypts with the locally installed CLI instead.--passwordadds 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 withSRIFT_NO_HISTORY=1or clear it withsrift 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
- ssh(1) manual: -J and -R options (OpenBSD / OpenSSH)
- scp(1) manual (OpenBSD / OpenSSH)
- Using IAP for TCP forwarding (Google Cloud)
- Allow and control permissions for SSH connections through Session Manager (AWS)
- Download and upload objects with presigned URLs (AWS)
- 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 --encryptRelated resources
- Moving Files Out of Docker Containers and Kubernetes Podsdocker cp, kubectl cp, volumes and outbound links compared for getting files out of containers and pods, including distroless and restricted clusters.How-to
- Command-Line File Transfer Tools Comparedscp, sftp, rsync, Magic Wormhole, croc and SRIFT compared on network requirements, encryption, authentication and the jobs each one fits.Comparison
- WebRTC Behind Corporate Firewalls and ProxiesWhy WebRTC fails on locked-down networks (UDP blocked, proxies, TLS inspection) and the fallbacks that still work over outbound port 443.Troubleshooting
- Automating Secure File Transfer: the SRIFT REST API and SDKsWhat makes a file-transfer API secure, and how to send and receive files from code with SRIFT: local REST endpoints, events and Python and Node.js SDKs.How-to
