SCP and SSH File Transfer
scp copies files over an existing SSH connection: the same auth, the same encryption, and the same host-key checking, in either direction. Reach for it the moment a box exposes SSH and you hold a password or a key, when you want an encrypted, integrity-checked transfer instead of the cleartext and truncation of HTTP or netcat.
Mental model: scp [options] SOURCE DEST, where either side can be user@host:path.
New to SSH transfer? Core concepts
- Remote spec:
user@host:path, the colon splits host from path, for exampleroot@TARGET_IP:/tmp/loot - Direction: source first, destination second, exactly like
cp - Path base: a remote path is relative to the login user’s home unless it starts with
/ - Recursive:
-rcopies a whole directory tree in either direction - Identity:
-i id_rsaselects a private key; it must bechmod 600or ssh refuses it - Port:
-P PORTfor scp and sftp (capital P); onlysshuses lowercase-pfor the port - Backends:
scpis a one-shot copy,sftpis an interactiveget/putsession, plainssh 'cmd'pipes bytes - Protocol note: OpenSSH 9+ runs scp over SFTP;
-Oforces the old scp/rcp protocol
When to reach for scp (and when not)
Reach for it when a target exposes SSH and you have a password or a key. It is the cleanest encrypted, authenticated, bidirectional transfer, moves whole directories with -r, and needs nothing extra stood up on either end.
Reach for something else before you have SSH at all (bootstrap over HTTP with wget/curl, over SMB, or with netcat), for large or repeated syncs where you want resume and delta transfer (rsync over ssh), or on a Windows target with no SSH server (SMB or the Windows download methods).
Install, update, verify#
sudo apt install -y openssh-client # Debian / Ubuntu / Kali
sudo dnf install -y openssh-clients # Fedora / RHEL
sudo apt install --only-upgrade -y openssh-client # update
ssh -V # OpenSSH version (9.x changed scp's backend to SFTP)
command -v scp sftp ssh # confirm all three are on PATH
scp, sftp, and ssh ship by default on Linux, macOS, and Windows 10+, all from the OpenSSH client package.
Flags you’ll actually use#
Skim these first; the cheat sheet below is these flags combined.
| Flag | Does | Flag | Does |
|---|---|---|---|
-r | Recurse into a directory | -i | Use this private key |
-P | Port (scp, capital P) | -p | Preserve mode + timestamps (scp) |
-C | Compress in transit | -l | Cap bandwidth (Kbit/s) |
-q | Quiet, no progress meter | -v | Verbose SSH trace |
-3 | Relay remote to remote via your box | -J | Hop through a jump host |
-O | Force legacy scp protocol | -c | Pick the cipher |
-o | Pass any ssh_config option | -F | Use a specific ssh config file |
Cheat sheet#
Each block starts simple and adds one flag at a time, so you can stop at the line that does the job.
# Upload: local -> target (stop at the line you need)
scp ./file USERNAME@TARGET_IP:/tmp/file # 1. one file
scp -r ./dir USERNAME@TARGET_IP:/tmp/dir # 2. + whole directory
scp -C ./big.iso USERNAME@TARGET_IP:/tmp/ # 3. + compress in transit
scp -P 2222 -i id_rsa ./file USERNAME@TARGET_IP:/tmp/ # 4. + key and custom port
# Download: target -> local
scp USERNAME@TARGET_IP:/home/USERNAME/flag.txt ./ # one file
scp -r USERNAME@TARGET_IP:/var/backups ./backups # a directory
scp 'USERNAME@TARGET_IP:/var/log/*.log' ./logs/ # remote glob (quote it)
# Keys, ports, host-key prompts
chmod 600 id_rsa # or ssh rejects the key
scp -i id_rsa USERNAME@TARGET_IP:/etc/passwd ./
scp -P 2222 ./file USERNAME@TARGET_IP:/tmp/ # non-default port (capital P)
scp -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
./file USERNAME@TARGET_IP:/tmp/ # lab: skip the prompt
# Through a jump host / between two boxes
scp -J USERNAME@BASTION_IP ./file USERNAME@TARGET_IP:/tmp/ # hop through a bastion
scp -3 USERNAME@HOST_A:/loot.tar USERNAME@HOST_B:/tmp/ # remote -> remote via you
# sftp: interactive multi-file
sftp -i id_rsa USERNAME@TARGET_IP
# put tool.sh /tmp/tool.sh get /var/www/config.php
# get -r /var/www ./www reget big.iso bye
# Pipe over ssh (no scp binary on the box)
cat tool.sh | ssh USERNAME@TARGET_IP 'cat > /tmp/tool.sh' # in
ssh USERNAME@TARGET_IP 'cat /tmp/loot.tar' > loot.tar # out
ssh USERNAME@TARGET_IP 'tar czf - /var/backups' > backups.tgz # whole dir out
# Verify + fallbacks
sha256sum tool.sh # source hash
ssh USERNAME@TARGET_IP 'sha256sum /tmp/tool.sh' # destination hash
scp -O ./file USERNAME@TARGET_IP:/tmp/ # force legacy protocol (OpenSSH 9+)
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect.
Upload a file to the target#
scp ./linpeas.sh USERNAME@TARGET_IP:/tmp/linpeas.sh
Local path first, user@host:dest second. A trailing /tmp/ (a directory) keeps the original name; give a full path to rename on the way. chmod +x on the target afterward.
Download a file from the target#
scp USERNAME@TARGET_IP:/home/USERNAME/flag.txt ./flag.txt
Same command with the sides swapped: the user@host: argument is now the source. A bare . drops the file in the current directory under its remote name.
Copy a whole directory#
scp -r USERNAME@TARGET_IP:/var/backups ./backups
-r recurses the tree in either direction. Without it scp skips directories and only warns, so a “not a regular file” message means you forgot the -r.
Authenticate with a recovered private key#
chmod 600 id_rsa
scp -i id_rsa USERNAME@TARGET_IP:/home/USERNAME/notes.txt ./
-i points at the key; ssh refuses a key that is group- or world-readable, hence the chmod 600 first. The USERNAME must match the account the key belongs to.
Connect on a non-default SSH port#
scp -P 2222 -i id_rsa ./tool USERNAME@TARGET_IP:/tmp/tool
scp and sftp spell the port -P (capital), while ssh uses -p (lowercase). Mixing them is the single most common scp mistake.
Transfer several files interactively with sftp#
sftp -i id_rsa USERNAME@TARGET_IP
sftp> put tool.sh /tmp/tool.sh
sftp> get -r /var/www ./www
sftp> bye
sftp gives a shell-like get/put session with tab completion, ls/lls, and cd/lcd. Better than scp when you are browsing and grabbing files as you go.
Pipe a file in when scp is missing#
cat tool.sh | ssh USERNAME@TARGET_IP 'cat > /tmp/tool.sh'
Streams the bytes through the SSH session itself, so it works on stripped images that run sshd but ship no scp binary.
Pull a directory out as a tarball#
ssh USERNAME@TARGET_IP 'tar czf - /var/backups' > backups.tgz
Tars and gzips on the target, streams to your stdout, and lands as one file locally. One session, the whole tree, and nothing written to the target’s disk.
Verify the transfer with a checksum#
sha256sum tool.sh
ssh USERNAME@TARGET_IP 'sha256sum /tmp/tool.sh'
SSH integrity-checks the channel, but a matching hash proves the file landed whole, which matters most after a pipe or an interrupted copy.
Skip the host-key prompt in a lab#
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ./file USERNAME@TARGET_IP:/tmp/
Suppresses the “authenticity of host” prompt and avoids polluting known_hosts on throwaway lab IPs. Never do this against real infrastructure; it defeats MITM protection.
Hop through a jump host#
scp -J USERNAME@BASTION_IP ./file USERNAME@TARGET_IP:/tmp/file
-J proxies the copy through a reachable bastion to a target you cannot route to directly. It is the shorthand for -o ProxyJump=USERNAME@BASTION_IP.
Copy directly between two remote hosts#
scp -3 USERNAME@HOST_A:/loot.tar USERNAME@HOST_B:/tmp/
-3 relays the bytes through your box, so both connections terminate on you, instead of asking host A to talk to host B, which is usually not allowed or not routable.
Force the legacy protocol on OpenSSH 9+#
scp -O ./file USERNAME@TARGET_IP:/tmp/file
OpenSSH 9 runs scp over SFTP by default; -O restores the old scp/rcp protocol when the target’s server or an unusual filename breaks the new one.
Resume an interrupted sftp transfer#
sftp USERNAME@TARGET_IP
sftp> reget /tmp/big.iso
reget continues a partial download from where it stopped; reput does the same for an upload. scp itself cannot resume, so use sftp or rsync for a flaky link.
Reading the output#
A running scp prints a one-line progress meter per file:
linpeas.sh 100% 746KB 1.2MB/s 00:00
| Column | Means |
|---|---|
linpeas.sh | The file being transferred |
100% | Percent complete |
746KB | Bytes moved so far |
1.2MB/s | Current throughput |
00:00 | Elapsed time, or ETA while running |
-q silences the meter for scripts. A percentage that freezes mid-transfer means the data channel is hung, almost always a firewall dropping the connection rather than a slow disk.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
Permissions 0644 ... too open | Key world-readable | chmod 600 id_rsa |
Connection refused | SSH closed / wrong port | Confirm 22, or -P the real port |
Permission denied (publickey) | Wrong key or user | Match the key to the right USERNAME |
scp: command not found on target | No scp binary | Pipe over ssh instead |
Host key verification failed | Reused lab IP changed keys | ssh-keygen -R TARGET_IP to clear it |
protocol error / scp hangs | OpenSSH 9 SFTP backend quirk | Add -O for the legacy protocol |
No such file or directory (remote) | Path is relative to home | Use an absolute path starting with / |
Gotchas#
-Pis the port,-ppreserves timestamps in scp, andsshuses lowercase-pfor the port. Swap them and scp silently does the wrong thing.- OpenSSH 9 moved scp onto the SFTP backend, so a copy that worked last year can fail against an old server or an odd filename;
-Oforces the legacy protocol. - Remote globs must be single-quoted (
'host:/path/*.log') so the pattern expands on the remote, not in your local shell first. - The remote path is relative to the login user’s home unless it starts with
/;scp file host:quietly lands it in$HOME. - A colon makes it remote, no colon makes it local:
scp file hostwith no:just copies to a local file literally namedhost. - scp has cp semantics with no confirmation, so reversing source and destination overwrites the wrong file instantly.
Pairs with#
nmap confirms port 22 is open before you try. Before you have SSH at all, bootstrap with http-file-transfer or netcat-file-transfer, then pivot to scp once you hold a shell and credentials. For large or repeated transfers reach for rsync over ssh, which resumes and sends only deltas, while chisel tunnels you to an SSH service that is not directly routable. See the file transfer methods tag for the full menu.