SSH, scp and rsync Transfers
Two objectives: accessing remote systems over SSH, and transferring files securely. Both are straightforward until quoting and trailing slashes get involved, and both of those are decided here by running the two forms side by side against a real second machine.
Essential Tools Guide 10 of 67 Beginner
- OSRHEL 10.0 (Coughlan)
- Kernel6.12.0-55.9.1.el10_0
- dnf4.20.0
- Flatpak1.16.0
- TimeAbout 15 min
- Reviewed23 August 2026
Written against the versions above. `scp` on RHEL 10 uses the SFTP protocol underneath rather than the legacy scp protocol. Behaviour for the commands here is unchanged; the difference shows up only in exotic cases like remote glob expansion.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| RHCSA-A01 | 192.168.0.31 | RHEL 10.0 (Coughlan) | Practice node (graded) - spare /dev/sda | 2 Core | 4 GB | 50 GB + 15 GB |
| RHCSA-B01 | 192.168.0.33 | RHEL 10.0 (Coughlan) | Practice node 2 - spare /dev/sdb | 2 Core | 4 GB | 50 GB + 15 GB |
Before you start
- Two machines, with ssh working between them.
- The session writes to
~on both and removes everything it created.
-
Running a command somewhere else
rhcsa-b01ssh host commandruns one command and returns, rather than opening a session. That is the form worth having, because it composes: the output comes back on stdout and can be piped, redirected or captured locally.11:02:31 up 2:41, 0 users, load average: 0.00, 0.01, 0.00 sysadmin 2Several commands in one go need quoting, because the semicolons would otherwise be interpreted by your local shell. And that raises the question the next step answers.
bash Example session ssh rhcsa-b01 hostnamerhcsa-b01ssh rhcsa-b01 'uptime; id -un; nproc' 16:38:46 up 36 min, 4 users, load average: 0.00, 0.00, 0.00sysadmin2ssh rhcsa-b01 'df -h /' | tail -2Filesystem Size Used Avail Use% Mounted on/dev/mapper/rhel-root 45G 4.8G 41G 11% /Expected resultCommands executing on the other machine.
Success conditionYou can run a command remotely without opening a session.
-
Which machine expands the variable
The same command, differing only in quote character:
ssh rhcsa-b01 "echo double quotes ran on: $(hostname)" double quotes ran on: rhcsa-a01ssh rhcsa-b01 'echo single quotes ran on: $(hostname)' single quotes ran on: rhcsa-b01Two different answers. Double quotes let your local shell substitute
$(hostname)before ssh is even called, so the remote machine received a string that already saidrhcsa-a01. Single quotes pass the text through untouched and the remote shell evaluates it.Neither is wrong - they are different tools. Local expansion is how you inject a local value into a remote command; remote expansion is how you ask about the remote machine. Getting them the wrong way round produces output that looks plausible and describes the wrong host, which is the worst kind of wrong.
Default to single quotes for remote commands, and reach for double deliberately.
bash Example session ssh rhcsa-b01 "echo double quotes ran on: $(hostname)"double quotes ran on: rhcsa-a01ssh rhcsa-b01 'echo single quotes ran on: $(hostname)'single quotes ran on: rhcsa-b01Expected resultThe same command naming two different hosts.
Success conditionYou control which shell expands what.
-
scp
made on rhcsa-a01 at 11:02:41scp local remote:pathpushes, and the file arrives:made on rhcsa-a01 at 11:02:41-rfor a directory:/home/sysadmin/tree/sub/deep.txtand reversing the arguments pulls instead of pushes:
Red Hat Enterprise Linux release 10.0 (Coughlan)The colon is what makes a path remote.
scp file host/pathwith a slash instead of a colon quietly copies to a local file calledhost/path, which is a classic five-minute confusion.~on the remote side is expanded by the remote shell, sorhcsa-b01:~/is that user's home there. An unqualifiedrhcsa-b01:means the same thing.bash Example session mkdir -p ~/xfer && cd ~/xfer && echo "made on $(hostname) at $(date +%T)" > note.txt && cat note.txtmade on rhcsa-a01 at 11:08:48cd ~/xfer && scp note.txt rhcsa-b01:~/ 2>&1 | tail -2; ssh rhcsa-b01 'cat ~/note.txt'made on rhcsa-a01 at 11:08:48cd ~/xfer && mkdir -p tree/sub && echo deep > tree/sub/deep.txt && scp -r tree rhcsa-b01:~/ 2>&1 | tail -1; ssh rhcsa-b01 'find ~/tree -type f'/home/sysadmin/tree/sub/deep.txtcd ~/xfer && scp rhcsa-b01:/etc/redhat-release pulled.txt 2>&1 | tail -1; cat pulled.txtRed Hat Enterprise Linux release 10.0 (Coughlan)Expected resultA file pushed, a directory pushed, and a file pulled back.
Success conditionYou can move files in both directions.
-
The rsync trailing slash
This is the one to get right, because both forms succeed and only one does what you meant.
Without a trailing slash - the directory itself is copied:
/home/sysadmin/nodash /home/sysadmin/nodash/tree /home/sysadmin/nodash/tree/sub /home/sysadmin/nodash/tree/sub/deep.txttreelanded *inside*nodash, producingnodash/tree/....With a trailing slash - the directory's *contents* are copied:
/home/sysadmin/withdash /home/sysadmin/withdash/sub /home/sysadmin/withdash/sub/deep.txtNo
treelevel. The contents went straight intowithdash.The rule: a trailing slash on the SOURCE means "the things in here", no slash means "this directory". The slash on the destination makes no difference.
Run the wrong one against an existing target and you get
backup/backup/etc/...instead ofbackup/etc/...- harmless but wrong, and on a restore it means the files are not where anything expects them.bash Example session cd ~/xfer && rsync -av tree rhcsa-b01:~/nodash/ 2>&1 | tail -4tree/sub/deep.txt sent 185 bytes received 91 bytes 552.00 bytes/sectotal size is 5 speedup is 0.02ssh rhcsa-b01 'find ~/nodash | sort'/home/sysadmin/nodash/home/sysadmin/nodash/tree/home/sysadmin/nodash/tree/sub/home/sysadmin/nodash/tree/sub/deep.txtcd ~/xfer && rsync -av tree/ rhcsa-b01:~/withdash/ 2>&1 | tail -4sub/deep.txt sent 175 bytes received 92 bytes 534.00 bytes/sectotal size is 5 speedup is 0.02ssh rhcsa-b01 'find ~/withdash | sort'/home/sysadmin/withdash/home/sysadmin/withdash/sub/home/sysadmin/withdash/sub/deep.txtExpected resultOne extra directory level in the no-slash form.
Success conditionYou can predict the resulting tree before you run it.
-
rsync only moves what changed
Run the identical command again:
sent 175 bytes received 92 bytes 534.00 bytes/sec total size is 5 speedup is 0.02No file list. Nothing changed, so nothing was transferred - only the metadata needed to work that out. Change one file and it reappears:
sub/deep.txt sent 234 bytes received 92 bytesThat is the difference from
scp, which copies everything every time. On a large tree over a slow link it is the only sensible choice, and it makes rsync safe to re-run - which in turn makes it safe to put in a cron job.-ais the flag to remember: archive mode, which is recursive plus preserving permissions, times, symlinks and ownership.-vshows what moved.-avis the pair worth memorising, and--deleteis the one to think twice about - it removes files from the destination that are gone from the source, which is either exactly what you want or a disaster.bash Example session cd ~/xfer && rsync -av tree/ rhcsa-b01:~/withdash/ 2>&1 | tail -3 sent 113 bytes received 13 bytes 252.00 bytes/sectotal size is 5 speedup is 0.04cd ~/xfer && echo "new content" >> tree/sub/deep.txt && rsync -av tree/ rhcsa-b01:~/withdash/ 2>&1 | tail -4sub/deep.txt sent 180 bytes received 42 bytes 444.00 bytes/sectotal size is 17 speedup is 0.08Expected resultA no-op run, then a run transferring only the changed file.
Success conditionYou can re-run a sync safely and cheaply.
Troubleshooting
A remote command reported the local hostname.
Why: Double quotes let the local shell expand it.
Fix:Single-quote the remote command.
rsync produced
dest/src/src/....Why: Missing trailing slash on the source.
Fix:
rsync -av src/ dest/copies contents;rsync -av src dest/copies the directory.scp created a local file named after the host.
Why: A
/where the:should be.Fix:
scp file host:/path- the colon is what makes it remote.Host key verification failed.
Why: The host is not in
~/.ssh/known_hostsunder the name you used.Fix:Connect once and accept, or
ssh-keyscan -H host >> ~/.ssh/known_hosts. Note the entry is per NAME - an entry for the IP does not cover the hostname.