The first remote copy usually feels magical: one command, a password prompt, and the file appears on another machine. The second copy is where the questions begin—did the trailing slash create an extra directory, will an interrupted 80 GB transfer restart, did ownership survive, and was that really the intended server? This article treats the path and verification details as part of the transfer, not cleanup afterward.

Prerequisites and trust checks

  • The source can reach the destination’s SSH service and the account is authorized to write the target directory.

  • SSH host identity is verified from a trusted fingerprint/source; never silence host-key checking to make automation “work.”

  • Enough destination space and inodes exist, and quotas/read-only mounts are understood.

  • Source data is stable or snapshot-consistent; copying live databases, VM images, mail stores, or changing application state can produce an unusable point-in-time result.

  • Required tools exist on the correct side: modern scp uses SFTP over SSH; remote-shell rsync normally requires compatible rsync programs on both hosts.

  • Ownership, ACL, xattr, hard-link, sparse-file, device, and security-label requirements are written down before selecting flags.

Test SSH before moving data

Source Linux hostbash
ssh -v deploy@files.example.com 'printf "connected as %s\n" "$(id -un)"'
... authenticated ...
connected as deploy

Authentication success is only the first check

  • -v exposes connection, host-key, and authentication decisions for diagnosis; it can reveal infrastructure details, so sanitize logs.

  • The quoted command runs remotely and confirms the effective account.

  • Verify the displayed host fingerprint through a trusted channel on first connection.

  • Prefer scoped keys or certificates, an agent with controlled forwarding, and server-side least privilege over passwords embedded in commands.

  • Test target write permissions separately without overwriting production data.

Put connection details in SSH configuration

~/.ssh/configtext
Host archive-host
    HostName files.example.com
    User deploy
    Port 22
    IdentityFile ~/.ssh/id_ed25519_archive
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

One reviewed alias reduces command drift

  • The alias archive-host can be used by ssh, scp, sftp, and rsync’s SSH transport.

  • IdentitiesOnly yes limits authentication attempts to configured identities and explicit agent identities.

  • Keep private keys readable only by their owner and never commit them.

  • Alive messages detect a dead connection; they do not make an interrupted transfer resumable.

  • Do not put StrictHostKeyChecking no or a disposable known-hosts file into routine automation.

Copy one file with scp

Source directorybash
scp -- ./report.csv archive-host:/srv/incoming/report.csv
report.csv                         100%   18MB  42.0MB/s   00:00

Both sides of the colon matter

  • The local ./report.csv is sent to the explicit remote path after archive-host:.

  • -- ends local option parsing, useful for filenames beginning with a hyphen.

  • Quote paths containing spaces or shell metacharacters; remote path interpretation depends on protocol/version and should be tested.

  • A successful exit means the client completed its work, not that an application can read or semantically use the file.

  • Modern OpenSSH scp uses SFTP by default; -O requests the legacy SCP protocol only for compatibility and reintroduces its quirks.

Copy a file back from remote to local

Local destination parentbash
scp archive-host:/srv/exports/report.csv ./downloads/report.csv
stat ./downloads/report.csv
report.csv                         100%   18MB  38.1MB/s   00:00
  File: ./downloads/report.csv
  Size: ...

Direction is determined by source and destination

  • The first operand is remote because it contains a recognized host prefix and colon; the second is local.

  • Create and permission the local parent directory before transfer.

  • A local filename containing a colon can be ambiguous; prefix it with ./ or use an absolute path.

  • stat confirms a local object exists but does not establish content identity; use checksums or application validation when required.

Copy a directory recursively with scp

Parent of local project directorybash
scp -r -- ./project archive-host:/srv/incoming/
... files transferred ...

Recursive scp is convenient, not a synchronizer

  • -r descends into the directory and copies it under the destination.

  • An interrupted recursive scp generally lacks rsync’s efficient resume/change-selection workflow.

  • It does not delete obsolete destination files or provide a reliable dry-run mirror plan.

  • For repeatable deployments or large trees, use rsync and stage releases atomically rather than copying into a live application directory.

Use rsync for repeatable directory transfers

Source hostbash
rsync -a --info=progress2 --human-readable ./project/ archive-host:/srv/incoming/project/
sending incremental file list
...
             1.24G 100% ...
sent ... received ... total size ... speedup ...

Archive mode is broad, but not everything

  • -a expands to recursive copying plus preservation of links, permissions, times, group, owner, and devices/specials where privileges allow.

  • Archive mode does not by itself include ACLs (-A), extended attributes (-X), hard-link relationships (-H), access times, or every platform-specific attribute.

  • The source trailing slash means “copy the contents of project”; without it, rsync normally creates a project level beneath the destination.

  • Both endpoints need suitable rsync versions when using remote-shell mode.

  • Transport is SSH for this host:path syntax unless configured otherwise; no standalone rsync daemon is required.

Preview changes before a consequential sync

Source hostbash
rsync -a --dry-run --itemize-changes ./project/ archive-host:/srv/incoming/project/
>f+++++++++ assets/new-logo.svg
>f.st...... index.html
cd+++++++++ docs/

A dry run is a plan based on current state

  • --dry-run performs selection without transferring file data; --itemize-changes explains proposed updates compactly.

  • State can change between preview and execution, so production automation should control writers or use snapshots/releases.

  • Review source/destination spelling and the trailing slash before trusting the itemized list.

  • Permissions, exclusions, mount boundaries, symlink rules, and remote-shell expansion can change the transfer set.

Resume an interrupted large transfer

Source hostbash
rsync -a --partial --partial-dir=.rsync-partial --info=progress2 ./dataset/ archive-host:/srv/archive/dataset/
... transfer progress ...

Keep partial data separate from final names

  • --partial retains interrupted work; --partial-dir stores it in a separate destination-side directory for reuse.

  • Rerunning the same command lets rsync evaluate and continue efficiently where its algorithm/version/file state permits.

  • Ensure the partial directory is excluded from application consumption, backup recursion, publication, and untrusted access.

  • For files being actively appended or modified, snapshot/stop writers rather than assuming resume flags create consistency.

  • -P is shorthand for --partial --progress, but an explicit partial directory is often operationally clearer.

Compression and bandwidth limits

Source hostbash
rsync -a --compress --bwlimit=20M ./logs/ archive-host:/srv/archive/logs/
... transfer limited near the configured rate ...

Compression can save bytes or waste CPU

  • --compress reduces compressible data in transit but often adds little for JPEG, video, ZIP, encrypted, or already compressed files.

  • --bwlimit limits rsync socket I/O approximately according to its documented units/averaging; validate on the installed version.

  • SSH may also compress if configured; avoid redundant compression decisions.

  • Measure CPU, elapsed time, network contention, and destination disk performance on representative data.

Mirroring with delete is destructive

Source hostbash
rsync -a --delete --dry-run --itemize-changes ./site/ archive-host:/srv/mirror/site/
# Run only after reviewing the complete dry-run output:
rsync -a --delete --itemize-changes ./site/ archive-host:/srv/mirror/site/
*deleting   obsolete.html
...

Risk level: destructive. Review the command before running it.

Delete makes the destination resemble the source

  • --delete removes destination entries absent from the transfer set; a reversed path or empty/wrong source can destroy data.

  • The dry run must use the same include/exclude, mount, symlink, permission, and delete options as the real operation.

  • Take and test a recoverable backup or snapshot, and confirm the exact destination through a guard.

  • Review rsync’s delete timing and excluded-file rules if using related options.

  • Do not mirror directly into a live release when a staged directory plus atomic switch provides safer rollback.

Transfer selected files and exclusions

Project rootbash
rsync -a --dry-run --itemize-changes \
  --exclude=.git/ \
  --exclude=.env \
  --exclude='*.tmp' \
  ./ archive-host:/srv/incoming/project/
... proposed files, excluding matched paths ...

Quote patterns so the local shell does not consume them

  • The quoted *.tmp reaches rsync as a filter instead of expanding against only the current directory.

  • Filter rules are evaluated relative to the transfer root with documented include/exclude semantics.

  • Excluding .env reduces one secret-copy risk but is not a substitute for inventorying credentials and generated artifacts.

  • Add the identical filters to preview and execution; store reviewed filter files for complex policies.

  • Remember that a pattern containing internal * characters is ordinary rsync syntax, not formatting markup.

Verify content after transfer

Source hostbash
sha256sum ./release.tar.zst
ssh archive-host 'sha256sum /srv/incoming/release.tar.zst'
91b...  ./release.tar.zst
91b...  /srv/incoming/release.tar.zst

Matching digests answer a narrow but valuable question

  • The same SHA-256 digest strongly indicates the two regular-file byte streams match.

  • Use an authenticated channel and trusted remote execution; a compromised endpoint can lie about files and hashes.

  • A digest does not verify filenames, permissions, owners, ACLs, xattrs, link structure, database consistency, or application semantics.

  • Rsync already verifies reconstruction of transferred files internally; --checksum changes pre-transfer change detection and can add heavy disk I/O.

  • For releases/backups, validate manifest signatures, extraction, startup/read tests, counts, metadata, and restoration as appropriate.

Stream a directory with tar over SSH

Parent of source directorybash
tar -C ./source -cf - . | ssh archive-host 'mkdir -p /srv/incoming/tree && tar -C /srv/incoming/tree -xf -'
No output on success; both pipeline stages should exit successfully.

Risk level: caution. Review the command before running it.

A stream avoids a temporary archive but raises the stakes

  • The local tar writes an archive to stdout; SSH carries it; remote tar extracts from stdin.

  • -C and relative member names avoid embedding an absolute source path.

  • Archive extraction can overwrite files and materialize symlinks/special entries; never extract an untrusted stream into a sensitive destination.

  • A plain shell pipeline can hide an earlier command failure unless the invoking shell uses/inspects pipeline status correctly.

  • GNU tar cannot use its ordinary post-write archive verification on a non-seekable pipe; perform independent destination checks.

SFTP for interactive and application workflows

Local shellbash
sftp archive-host
Connected to archive-host.
sftp> pwd
sftp> lpwd
sftp> put report.csv /srv/incoming/report.csv
sftp> get /srv/exports/result.csv ./result.csv
sftp> bye

Always distinguish local and remote working directories

  • pwd and remote path commands refer to the server; lpwd and l-prefixed commands refer to local state.

  • SFTP uses SSH authentication/encryption but exposes file operations rather than an interactive remote shell.

  • Batch mode is useful for automation only with explicit error handling, host verification, logging, and idempotency.

  • Server policy may allow SFTP while denying shell commands, which prevents rsync/tar-over-SSH even though SFTP works.

Metadata and filesystem boundaries

  • Ordinary users cannot recreate arbitrary owners, device nodes, capabilities, or security labels.

  • ACLs and xattrs need explicit rsync options plus support/permissions on both filesystems and rsync builds.

  • Hard links require preservation logic; otherwise linked names may become independent copies.

  • Sparse files can expand dramatically unless the chosen tool/options/filesystem preserve holes.

  • Symlink following versus preservation changes both data copied and escape risk; audit links that point outside the source tree.

  • Do not cross mounted filesystems accidentally; decide whether mount points, bind mounts, proc/sys/dev, containers, and network filesystems belong.

  • Filenames can contain spaces, newlines, leading hyphens, glob characters, and non-UTF-8 bytes; prefer null-safe manifests/tool-native selection over shell loops.

Live data needs application consistency

  • Filesystem-level copying does not create a consistent database backup while transactions continue.

  • Use database-native backup/snapshot procedures and test restoration.

  • For VMs, containers, mail stores, repositories, and object indexes, follow application quiesce/snapshot/export guidance.

  • Rsync’s second pass can narrow changes but does not create an atomic multi-file point in time.

  • Stage received data, validate it, then publish through an atomic rename/symlink/release mechanism when possible.

Troubleshooting map

  • Permission denied (publickey): inspect selected identity, agent, username, server authorization, permissions, and verbose SSH output.

  • Host key changed: stop and verify whether the server was rebuilt, DNS changed, or a man-in-the-middle attack is possible; never delete the warning blindly.

  • Connection timed out/refused: verify route, firewall/security group, hostname, port, SSH service, and bastion/VPN requirements.

  • No space left: check bytes, inodes, quotas, snapshots, and the actual destination filesystem.

  • Extra directory level: compare rsync source paths with and without the trailing slash.

  • Files recopied every run: compare timestamps/resolution, clock, size, metadata, generated files, filesystem behavior, and rsync versions.

  • Remote rsync not found: install an approved compatible package or use SFTP/scp/tar with understood limitations.

  • Transfer completed but app fails: validate ownership, mode, ACL/xattr, SELinux/AppArmor context, links, completeness, application format, and atomic publication.

Production transfer checklist

  • Exact source, destination, direction, trailing-slash meaning, and remote account are reviewed.

  • Host key is verified; credentials are scoped, protected, and not exposed in arguments/logs.

  • Free space, inodes, quotas, mounts, permissions, and tool versions are checked.

  • The data is quiesced/snapshotted or application-exported for consistency.

  • Metadata requirements determine scp/rsync/SFTP/tar options.

  • Destructive delete/overwrite operations have an equivalent dry run and recoverable backup.

  • Interrupted transfer behavior and retry/idempotency are understood.

  • Counts, hashes/manifests, metadata, and application/restoration checks prove the result.

  • Logs are retained without secrets, and temporary/partial data is cleaned through a safe policy.

Primary references

  • The OpenBSD/OpenSSH `scp(1)` manual documents modern SFTP transport, legacy compatibility, options, paths, and exit behavior.

  • The upstream `rsync(1)` manual defines archive mode, trailing slashes, filters, partial files, delete behavior, checksums, and metadata limits.

  • The GNU tar manual documents archive creation/extraction, member selection, absolute paths, comparison, and why streamed archives need separate verification.

  • Use the installed ssh(1), ssh_config(5), sftp(1), rsync(1), and tar(1) manuals because distribution versions and supported algorithms/options differ.