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 example root@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: -r copies a whole directory tree in either direction
  • Identity: -i id_rsa selects a private key; it must be chmod 600 or ssh refuses it
  • Port: -P PORT for scp and sftp (capital P); only ssh uses lowercase -p for the port
  • Backends: scp is a one-shot copy, sftp is an interactive get/put session, plain ssh 'cmd' pipes bytes
  • Protocol note: OpenSSH 9+ runs scp over SFTP; -O forces 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.

FlagDoesFlagDoes
-rRecurse into a directory-iUse this private key
-PPort (scp, capital P)-pPreserve mode + timestamps (scp)
-CCompress in transit-lCap bandwidth (Kbit/s)
-qQuiet, no progress meter-vVerbose SSH trace
-3Relay remote to remote via your box-JHop through a jump host
-OForce legacy scp protocol-cPick the cipher
-oPass any ssh_config option-FUse 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
ColumnMeans
linpeas.shThe file being transferred
100%Percent complete
746KBBytes moved so far
1.2MB/sCurrent throughput
00:00Elapsed 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#

ProblemCauseFix
Permissions 0644 ... too openKey world-readablechmod 600 id_rsa
Connection refusedSSH closed / wrong portConfirm 22, or -P the real port
Permission denied (publickey)Wrong key or userMatch the key to the right USERNAME
scp: command not found on targetNo scp binaryPipe over ssh instead
Host key verification failedReused lab IP changed keysssh-keygen -R TARGET_IP to clear it
protocol error / scp hangsOpenSSH 9 SFTP backend quirkAdd -O for the legacy protocol
No such file or directory (remote)Path is relative to homeUse an absolute path starting with /

Gotchas#

  • -P is the port, -p preserves timestamps in scp, and ssh uses lowercase -p for 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; -O forces 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 host with no : just copies to a local file literally named host.
  • 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.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop