Lesson 11 of 14
SSH, scp, and rsync
SSH opens an encrypted connection to another machine and runs commands there. scp and rsync use the same connection to copy files. This lesson covers key-based login, remote commands, host aliases, the agent, port forwarding, and two ways to copy files, with real output for each.
| Command | Purpose |
|---|---|
ssh-keygen |
Create a key pair and print key fingerprints |
~/.ssh/authorized_keys |
File on the server that lists the public keys allowed to log in |
ssh user@host |
Log in, or run one command on the remote machine |
~/.ssh/config |
Give hosts short names and per-host settings |
ssh-agent, ssh-add |
Hold decrypted keys in memory so a passphrase is typed once |
ssh -L |
Forward a local port to a port reachable from the remote machine |
scp |
Copy files and directories over SSH |
rsync -avz |
Copy only what changed, keep metadata, compress in transit |
rsync --dry-run, --delete |
Preview a copy, and make the destination match the source |
How the examples were run
SSH needs a server. The examples start a private sshd on 127.0.0.1 port 2222, so "the remote machine" is the same computer. A reader repeats them with a real server by replacing 127.0.0.1, the port, and the user name.
The setup used a scratch home directory, shown below as /home/dev, and a throwaway host key and configuration. The server was started as root and stopped at the end.
mkdir -p ~/sshd-demo
ssh-keygen -q -t ed25519 -N "" -f ~/sshd-demo/host_key
cat > ~/sshd-demo/sshd_config <<CFG
Port 2222
ListenAddress 127.0.0.1
HostKey $HOME/sshd-demo/host_key
PidFile $HOME/sshd-demo/sshd.pid
PasswordAuthentication no
UsePAM no
Subsystem sftp internal-sftp
CFG
sudo mkdir -p /run/sshd
sudo /usr/sbin/sshd -f ~/sshd-demo/sshd_configBecause the server and the client share one file system, remote paths in the examples are absolute (/home/dev/demo/...). On a real server, a relative remote path such as backup/ is resolved from the remote home directory.
Output that changes on every run: key fingerprints, randomart images, the encoded key text, and file times. The text says where this matters.
Example 1: Generate a key pair with ssh-keygen
A key pair has a private key that stays on the client and a public key that is placed on servers. Logging in with the pair needs no password to cross the network.
ssh-keygen -t ed25519 -C "demo@example" -f ~/.ssh/id_ed25519_demo -N ""
ls -l ~/.ssh
cat ~/.ssh/id_ed25519_demo.pub
ssh-keygen -l -f ~/.ssh/id_ed25519_demo.pubGenerating public/private ed25519 key pair.
Your identification has been saved in /home/dev/.ssh/id_ed25519_demo
Your public key has been saved in /home/dev/.ssh/id_ed25519_demo.pub
The key fingerprint is:
SHA256:6CYdey7vv4GVH4wsMnzRWwjUFcuUYe/MEq2fSBV+ZDI demo@example
The key's randomart image is:
+--[ED25519 256]--+
| .o. .*+E o|
| o.=.= * |
| . o = = .|
| . . o * B . |
| * S * * + |
| o * + o = . |
| . = o . o o |
| o.o . |
| ++.o. |
+----[SHA256]-----+
total 8
-rw------- 1 root root 399 Oct 3 15:20 id_ed25519_demo
-rw-r--r-- 1 root root 94 Oct 3 15:20 id_ed25519_demo.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPcU9HdeLevyWMe3C++QpY6eKyoKSXW17DZNCAOjfYkN demo@example
256 SHA256:6CYdey7vv4GVH4wsMnzRWwjUFcuUYe/MEq2fSBV+ZDI demo@example (ED25519)The fingerprint, randomart, key text, owner, and time differ on every run. The sizes (399 and 94 bytes) are stable for ed25519 with this comment.
| Option | Meaning |
|---|---|
-t ed25519 |
Key type. Ed25519 keys are short and fast. -t rsa -b 4096 creates an RSA key for older servers. |
-C "demo@example" |
Comment stored at the end of the public key. It labels the key and has no effect on security. |
-f path |
File name for the private key. The public key gets .pub added. |
-N "" |
New passphrase. An empty string means no passphrase. A key meant for real use gets a passphrase (Example 6). |
-l -f file |
Print the fingerprint of an existing key instead of making a new one. |
Reading the output: the private key file has mode 600 (-rw-------), so only its owner can read it. The public key has mode 644 and is safe to share. The fingerprint is a short hash of the public key. Two people compare fingerprints to confirm they mean the same key. The randomart image is a picture of that hash, meant for quick visual comparison.
Without -f, ssh-keygen asks for a file name and suggests ~/.ssh/id_ed25519. The default name is the one ssh tries automatically, so a key with a custom name has to be named with -i or in ~/.ssh/config (Examples 3 and 4).
Example 2: Authorize the key with authorized_keys
A server logs a user in without a password when the user's public key appears in ~/.ssh/authorized_keys on the server. The file holds one public key per line.
cat ~/.ssh/id_ed25519_demo.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
ls -ld ~/.ssh
ls -l ~/.ssh/authorized_keys
wc -l ~/.ssh/authorized_keysdrwx------ 2 root root 4096 Oct 3 15:20 /home/dev/.ssh
-rw------- 1 root root 94 Oct 3 15:20 /home/dev/.ssh/authorized_keys
1>> appends, so existing keys stay. A single > would replace the file and lock out every other key. wc -l counts one line: one authorized key.
On a real server the public key is sent with ssh-copy-id -i ~/.ssh/id_ed25519_demo.pub user@host. That command logs in with a password once, appends the key to the remote authorized_keys, and sets safe permissions. Here the client and the server use the same file, so a local append is enough.
The chmod lines matter. By default sshd has StrictModes on and refuses a key file that other users can write to, or a ~/.ssh directory that others can write to. The modes 700 for the directory and 600 for the file satisfy it. The demo server runs with StrictModes no only because its files sit in a scratch directory.
Example 3: Log in to localhost with ssh
The basic form is ssh user@host. A command after the host runs on the remote machine and the connection ends when it finishes.
ssh -p 2222 -i ~/.ssh/id_ed25519_demo root@127.0.0.1 "echo connected"Host key verification failed.The first connection to a server shows its host key fingerprint and asks for confirmation. The command ran without a terminal for a prompt, so it stopped. The host key proves the server's identity, in the same way a user key proves the client's. Accepting it once stores it in ~/.ssh/known_hosts, and later connections compare against the stored copy.
ssh -p 2222 -i ~/.ssh/id_ed25519_demo -o StrictHostKeyChecking=accept-new root@127.0.0.1 "echo connected"
cut -c1-60 ~/.ssh/known_hosts
ssh -p 2222 -i ~/.ssh/id_ed25519_demo -o BatchMode=yes root@127.0.0.1 "echo second time"Warning: Permanently added '[127.0.0.1]:2222' (ED25519) to the list of known hosts.
connected
|1|Qc41XUKzODeZChx2eaRUjafQRIQ=|kWwDUdxhsS62pNhkibjsNutLyWM=
second time| Option | Meaning |
|---|---|
-p 2222 |
Server port. The default is 22. |
-i file |
Private key to offer. Without it, ssh tries the default key names and any keys in the agent. |
-o StrictHostKeyChecking=accept-new |
Store the host key of a new server without asking. A changed key for a known server is still refused. |
-o BatchMode=yes |
Never prompt. Fail at once if a password, passphrase, or confirmation would be needed. Useful in scripts. |
The known_hosts line is hashed (it starts with |1|), so the file does not list the names of the servers a user has visited. The value differs on every run.
To see how the login happened, -v prints debug messages. The filter below keeps the three lines that show the key exchange:
ssh -p 2222 -i ~/.ssh/id_ed25519_demo -v -o BatchMode=yes root@127.0.0.1 true 2>&1 | grep -E "Authenticated|Offering public|Server accepts key"debug1: Offering public key: /home/dev/.ssh/id_ed25519_demo ED25519 SHA256:6CYdey7vv4GVH4wsMnzRWwjUFcuUYe/MEq2fSBV+ZDI explicit
debug1: Server accepts key: /home/dev/.ssh/id_ed25519_demo ED25519 SHA256:6CYdey7vv4GVH4wsMnzRWwjUFcuUYe/MEq2fSBV+ZDI explicit
Authenticated to 127.0.0.1 ([127.0.0.1]:2222) using "publickey".The client offers the public key, the server finds it in authorized_keys and accepts it, and the client then proves it holds the private key. -v goes to stderr, so 2>&1 joins it to the pipe.
When no usable key exists, the server refuses the login:
ssh -p 2222 -o IdentitiesOnly=yes -i /nonexistent -o BatchMode=yes root@127.0.0.1 trueWarning: Identity file /nonexistent not accessible: No such file or directory.
root@127.0.0.1: Permission denied (publickey,keyboard-interactive).IdentitiesOnly=yes stops ssh from trying other keys. The text in parentheses lists the methods the server would accept. Permission denied followed by a method list is the usual sign that the server did not find a matching key. The exit code is 255, the value ssh uses for its own errors.
On macOS the same commands work with the system ssh. The file locations are the same.
Example 4: Name a host in ~/.ssh/config
Typing the port, user, and key on every command is error-prone. ~/.ssh/config stores them under a short name.
cat > ~/.ssh/config <<'CFG'
Host demo
HostName 127.0.0.1
Port 2222
User root
IdentityFile ~/.ssh/id_ed25519_demo
IdentitiesOnly yes
StrictHostKeyChecking accept-new
CFG
chmod 600 ~/.ssh/config
ssh demo "echo via config"
ssh -G demo | grep -E "^(hostname|port|user|identityfile|identitiesonly) "via config
user root
hostname 127.0.0.1
port 2222
identitiesonly yes
identityfile /home/dev/.ssh/id_ed25519_demo| Line | Meaning |
|---|---|
Host demo |
Starts a block. The name demo is what is typed on the command line. Patterns such as Host *.example.com or Host * match many names. |
HostName |
The real address to connect to. |
Port, User |
Replace -p and the user@ prefix. |
IdentityFile |
Private key for this host. |
IdentitiesOnly yes |
Offer only the listed key. |
StrictHostKeyChecking accept-new |
Same behavior as in Example 3, stored per host. |
ssh -G demo prints the final settings ssh would use for that name, one per line, without connecting. The filtered lines confirm the block was read. In the full output ssh -G also prints many defaults.
The first value found for an option wins, and blocks are read from the top of the file. Specific hosts therefore go first and Host * defaults go last. The aliases also work for scp and rsync, which Examples 8 to 12 use.
Example 5: Run a remote command
A command after the host name runs there, and its output returns to the local terminal. The exit code of the remote command becomes the exit code of ssh.
mkdir -p /home/dev/demo/site
echo "hello from the remote service" > /home/dev/demo/site/index.html
echo "p" > /home/dev/demo/site/page2.html
ssh demo "echo one; echo two"
ssh demo "ls /home/dev/demo/site"
ssh demo "exit 3"; echo "exit code: $?"
ssh demo "grep -c ^ /etc/hostname"
echo "alpha beta" | ssh demo "tr a-z A-Z"
ssh demo "cat /nonexistent"; echo "exit code: $?"one
two
index.html
page2.html
exit code: 3
1
ALPHA BETA
cat: /nonexistent: No such file or directory
exit code: 1| Case | What it shows |
|---|---|
"echo one; echo two" |
The quotes keep the whole string together. The remote shell then splits it at ;, so both commands run remotely. |
ssh demo "exit 3" |
The remote exit status 3 comes back as the local $?. |
echo ... | ssh demo "tr ..." |
Standard input of the local pipe becomes standard input of the remote command. Output returns the same way, so ssh works as a link in a pipeline. |
cat /nonexistent |
The error text comes from the remote cat and arrives on the local stderr. The exit code 1 is the remote one. |
Quoting decides where a variable is expanded. In ssh demo "ls $DIR" the local shell replaces $DIR before sending. In ssh demo 'ls $DIR' the single quotes send the text unchanged and the remote shell expands it. For commands with pipes, quote the whole command so the pipe runs on the remote side: ssh demo "ps aux | wc -l" counts remote processes, while ssh demo ps aux | wc -l pipes the remote output into a local wc.
Without a command, ssh demo opens an interactive shell. With a command and a need for a prompt (for example sudo or top), -t requests a terminal: ssh -t demo top.
Example 6: Hold a passphrase-protected key in ssh-agent
A passphrase encrypts the private key file, so a stolen file is useless without it. Typing the passphrase on every login is tedious, so ssh-agent keeps the decrypted key in memory for a session. ssh-add loads keys into the agent.
The demo creates a key with a passphrase and authorizes it. The passphrase appears on the command line here only so the demo runs without prompts. On a real key, omit -N and type the passphrase at the prompt, because command-line arguments are visible to other users in the process list.
ssh-keygen -q -t ed25519 -C "agent-demo" -N "demo-pass" -f ~/.ssh/id_ed25519_agent
cat ~/.ssh/id_ed25519_agent.pub >> ~/.ssh/authorized_keys
ssh -p 2222 -i ~/.ssh/id_ed25519_agent -o IdentitiesOnly=yes -o BatchMode=yes root@127.0.0.1 "echo hi"root@127.0.0.1: Permission denied (publickey,keyboard-interactive).With BatchMode=yes the client cannot ask for the passphrase, so it cannot use the key and the login fails. An agent solves this.
eval "$(ssh-agent -s)"
ssh-add -l
ssh-add ~/.ssh/id_ed25519_agent
ssh-add -l
ssh -p 2222 -o BatchMode=yes root@127.0.0.1 "echo hi from agent key"
ssh-add -D
ssh-add -lAgent pid 11873
The agent has no identities.
Identity added: /home/dev/.ssh/id_ed25519_agent (agent-demo)
256 SHA256:Iz+e34+CqCdnYR2ddrZ2UUGBDxNTlU6hMDPQWyM3ydY agent-demo (ED25519)
hi from agent key
All identities removed.
The agent has no identities.The agent process id and the fingerprint vary. In the recorded run, ssh-add read the passphrase from a helper program (the SSH_ASKPASS variable) instead of a prompt. In a terminal, ssh-add asks for it once.
| Command | Meaning |
|---|---|
eval "$(ssh-agent -s)" |
Start an agent. It prints shell commands that set SSH_AUTH_SOCK and SSH_AGENT_PID. eval runs them so this shell and its children can find the agent. |
ssh-add file |
Decrypt the key (asks for the passphrase) and keep it in the agent. |
ssh-add -l |
List loaded keys by fingerprint. Exit code 1 and "no identities" mean the agent is empty. |
ssh-add -D |
Remove all keys from the agent. |
ssh-agent -k |
Stop the agent started in this shell. |
The login line has no -i option and no passphrase. ssh asked the agent through SSH_AUTH_SOCK, and the agent signed the login challenge without exposing the private key. Agent forwarding (ssh -A) lets a remote machine use the local agent for a further hop. It gives anyone with admin rights on that machine the ability to use the loaded keys while the connection lasts, so it is limited to hosts that are trusted.
On macOS, ssh-add --apple-use-keychain stores the passphrase in the system keychain, and UseKeychain yes with AddKeysToAgent yes in the config file loads it automatically. These are options of the Apple build of OpenSSH.
Example 7: Forward a local port with ssh -L
Local port forwarding makes a port on the client reach a port on, or reachable from, the server. It lets a local tool talk to a service that is not exposed to the network, such as a database or an internal web page.
The demo runs a small web server on port 8000 as the "remote service". Port 8080 is the local end of the tunnel.
cd /home/dev/demo/site && python3 -m http.server 8000 --bind 127.0.0.1 >/dev/null 2>&1 &
sleep 1
curl -s http://127.0.0.1:8080/ || echo "nothing listens on 8080"
ssh -f -N -L 8080:127.0.0.1:8000 -o ExitOnForwardFailure=yes demo
curl -s http://127.0.0.1:8080/
ss -tln | grep -E ":(8080|8000) " | awk '{print $1, $4}'nothing listens on 8080
hello from the remote service
LISTEN 127.0.0.1:8000
LISTEN 127.0.0.1:8080Before the tunnel, nothing listens on 8080 and curl fails. After ssh -f -N -L ..., the same curl returns the page that the service on port 8000 serves. ss -tln lists listening TCP sockets: port 8000 is the service, port 8080 is the tunnel end held by ssh.
| Part | Meaning |
|---|---|
-L 8080:127.0.0.1:8000 |
Listen on local port 8080. Send each connection through the SSH link, and have the server connect to 127.0.0.1:8000 as seen from the server. |
-N |
Do not run a remote command. The connection exists only for the forward. |
-f |
Move ssh to the background after authentication, so the shell is free. |
-o ExitOnForwardFailure=yes |
Exit with an error if the local port cannot be bound, instead of staying connected without a forward. |
The address after the first port is resolved on the server. For a database that only the server can reach, -L 5433:db.internal:5432 lets a local client connect to 127.0.0.1:5433. The local end listens on 127.0.0.1 by default, so other machines cannot use it unless the listen address is given (-L 0.0.0.0:8080:...) and the server allows it.
If the remote side has nothing listening, the tunnel still opens, and the connection through it is closed:
ssh -N -f -L 8080:127.0.0.1:1 demo
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/000curl prints 000 when no HTTP response arrived. The tunnel was up, and the server could not connect to port 1.
A forward started with -f keeps running until its process is stopped. Find it with pgrep -f "L 8080" and end it with kill. The demo stopped both the tunnel and the web server before moving on. The reverse direction, -R, opens a port on the server that leads back to the client. A dynamic forward, -D 1080, turns the connection into a SOCKS proxy.
Example 8: Copy files with scp
scp copies files between machines using SSH for transport. It uses the same host names and the same ~/.ssh/config entries as ssh.
cd /home/dev/demo
mkdir -p src/lib out remote
echo "line 1" > notes.txt
printf 'a\nb\n' > src/main.txt
echo lib > src/lib/util.txt
scp notes.txt demo:/home/dev/demo/remote/
ls /home/dev/demo/remotenotes.txtscp printed nothing: when output goes to a script or a pipe, progress meters are off, and success is silent. The file is now in the destination directory. The form is scp source destination, and a path that contains host: is the remote one.
Downloading reverses the arguments:
scp demo:/home/dev/demo/remote/notes.txt /home/dev/demo/out/copy.txt && cat out/copy.txtline 1Directories need -r (recursive):
scp -r src demo:/home/dev/demo/remote/
ls -R /home/dev/demo/remote/home/dev/demo/remote:
notes.txt
src
/home/dev/demo/remote/src:
lib
main.txt
/home/dev/demo/remote/src/lib:
util.txtWithout a config entry, the port and the key are given on the command line. scp uses a capital -P for the port, while ssh uses lowercase -p. Lowercase -p in scp means "preserve times and modes".
scp -P 2222 -i ~/.ssh/id_ed25519_demo notes.txt root@127.0.0.1:/home/dev/demo/remote/n2.txtThe command printed nothing and exited with status 0.
| Option | Meaning |
|---|---|
-r |
Copy a directory and its contents. |
-P port |
Remote port (capital P). |
-i file |
Private key. |
-p |
Keep modification times and modes. |
-C |
Compress data in transit. |
-q |
Quiet. Hides the progress meter in a terminal. |
scp always copies every file in full. It has no way to skip files that are already present, and it cannot delete. These gaps are what rsync fills. Recent OpenSSH versions run scp over the SFTP protocol, so the behavior of remote glob characters such as * depends on the version. A quoted remote pattern such as scp 'demo:/logs/*.log' . works in current releases.
Example 9: Synchronize a directory with rsync -avz
rsync compares the source and the destination and sends only what differs. It is the usual tool for repeated copies of the same directory.
cd /home/dev/demo
rm -rf remote && mkdir -p remote
mkdir -p project/docs
echo "readme v1" > project/README.md
echo "guide" > project/docs/guide.md
echo "debug" > project/app.log
rsync -avz project/ demo:/home/dev/demo/remote/project/sending incremental file list
created directory /home/dev/demo/remote/project
./
README.md
app.log
docs/
docs/guide.md
sent 315 bytes received 153 bytes 312.00 bytes/sec
total size is 22 speedup is 0.05| Option | Meaning |
|---|---|
-a |
Archive mode. Short for -rlptgoD: recurse, copy symlinks as symlinks, and keep permissions, times, group, owner, and device files. |
-v |
List each transferred file. |
-z |
Compress data during transfer. It saves bandwidth on slow links and costs CPU on fast ones. |
The summary line sent 315 bytes received 153 bytes counts bytes on the wire. speedup is the total size divided by the bytes sent. It is below 1 here because the files are tiny and the protocol overhead dominates. The byte counts and the rate vary between runs.
Change one file and run the same command again:
echo "readme v2" > project/README.md
rsync -avz project/ demo:/home/dev/demo/remote/project/
rsync -avz project/ demo:/home/dev/demo/remote/project/sending incremental file list
README.md
sent 218 bytes received 42 bytes 520.00 bytes/sec
total size is 22 speedup is 0.08
sending incremental file list
sent 157 bytes received 13 bytes 340.00 bytes/sec
total size is 22 speedup is 0.13The second run sent only README.md, the changed file. The third run listed no files at all, because nothing differed. rsync decides by comparing size and modification time first, and -a preserves times, which makes this comparison reliable. For large changed files, rsync also sends only the changed blocks.
Patterns skip files with --exclude:
rsync -avz --exclude "*.log" project/ demo:/home/dev/demo/remote/clean/
ls -A /home/dev/demo/remote/cleansending incremental file list
created directory /home/dev/demo/remote/clean
./
README.md
docs/
docs/guide.md
sent 257 bytes received 132 bytes 778.00 bytes/sec
total size is 16 speedup is 0.04
README.md
docsapp.log was not copied. A pattern with a slash, such as --exclude "docs/", matches paths. --exclude-from=file reads patterns from a file.
rsync uses ssh for remote copies by default. -e "ssh -p 2222 -i key" selects a different remote shell command and options when no config entry exists.
Example 10: Preview a copy with --dry-run
--dry-run (short form -n) goes through the whole comparison and reports what would change, without changing anything. It is the safe first step before any large or destructive copy.
echo "new file" > project/docs/new.md
rsync -avzn project/ demo:/home/dev/demo/remote/project/
ls /home/dev/demo/remote/project/docssending incremental file list
docs/
docs/new.md
sent 193 bytes received 19 bytes 141.33 bytes/sec
total size is 31 speedup is 0.15 (DRY RUN)
guide.mdThe summary ends with (DRY RUN). The listing shows docs/new.md would be sent, and the following ls confirms the remote directory still holds only guide.md.
--itemize-changes (-i) adds a column that says why each line is listed:
rsync -avz --itemize-changes --dry-run project/ demo:/home/dev/demo/remote/project/sending incremental file list
.d..t...... docs/
<f+++++++++ docs/new.md
sent 193 bytes received 19 bytes 424.00 bytes/sec
total size is 31 speedup is 0.15 (DRY RUN)| Code | Meaning |
|---|---|
< |
The file would be sent to the remote side. (> marks a file received.) |
f, d |
The item is a file or a directory. |
+++++++++ |
The item is new. Each position stands for an attribute, and plus signs mean all are new. |
.d..t...... |
A directory whose modification time (t) differs. Nothing else changes. |
The directory line appears because adding a file changes the directory's time, and -a keeps times in step.
Example 11: Mirror a directory with --delete
By default rsync only adds and updates files. --delete also removes files from the destination that no longer exist in the source, which makes the destination an exact mirror.
Remove a source file, then preview before running:
rm project/docs/guide.md
rsync -avz --dry-run --delete project/ demo:/home/dev/demo/remote/project/sending incremental file list
deleting docs/guide.md
docs/
docs/new.md
sent 173 bytes received 43 bytes 432.00 bytes/sec
total size is 25 speedup is 0.12 (DRY RUN)The preview shows deleting docs/guide.md. The listing also shows docs/new.md again, because the earlier run in Example 10 was a dry run and sent nothing. Run it for real after reading the list:
rsync -avz --delete project/ demo:/home/dev/demo/remote/project/
ls /home/dev/demo/remote/project/docssending incremental file list
deleting docs/guide.md
docs/
docs/new.md
sent 230 bytes received 63 bytes 586.00 bytes/sec
total size is 25 speedup is 0.09
new.mdThe remote docs directory now has new.md only, the same as the source.
--delete removes data permanently, and a wrong destination path removes the wrong files. These habits limit the risk:
- Run with
--dry-runfirst and read everydeletingline. - Check that the source is the directory intended. An empty or missing source with
--deleteempties the destination. - Use
--backup --backup-dir=DIRto move replaced and deleted files into a directory instead of removing them. - Use
--max-delete=Nto stop if more than N files would be deleted.
Files that are excluded are also protected from deletion on the destination, unless --delete-excluded is given.
Example 12: Trailing slashes in rsync paths
A trailing slash on the source changes what rsync copies. This rule is the most common source of surprises with rsync.
src/means the contents ofsrc.src(no slash) means the directorysrcitself.
The slash on the destination does not change the result in the cases below. The demo copies a small tree to a local destination, which follows the same rule as the remote form.
cd /home/dev/demo
mkdir -p a/inner b
echo x > a/inner/f.txt
echo y > a/top.txt
rsync -a --dry-run -v a/ b/sending incremental file list
top.txt
inner/
inner/f.txt
sent 141 bytes received 26 bytes 334.00 bytes/sec
total size is 4 speedup is 0.02 (DRY RUN)With a/, the listed names are top.txt and inner/. The contents of a would land directly inside b. Run both forms for real and list the result with find:
rsync -a a b/ && find b | sort
rm -rf b && mkdir b
rsync -a a/ b/ && find b | sortb
b/a
b/a/inner
b/a/inner/f.txt
b/a/top.txt
b
b/inner
b/inner/f.txt
b/top.txt| Command | Result |
|---|---|
rsync -a a b/ |
Creates b/a/.... The directory a is placed inside b. |
rsync -a a/ b/ |
Creates b/top.txt and b/inner/.... The contents of a go into b. |
The dry run for the no-slash form lists names that start with a/, which confirms the extra level:
rsync -a --dry-run -v a bsending incremental file list
a/
a/top.txt
a/inner/
a/inner/f.txt
sent 152 bytes received 30 bytes 364.00 bytes/sec
total size is 4 speedup is 0.02 (DRY RUN)The trailing slash matters most together with --delete. rsync -a --delete project/ host:backup/ makes everything in backup match project, including the removal of unrelated files that were already in backup. rsync -a --delete project host:backup/ makes only backup/project match, and leaves the rest of backup alone. Mixing the two forms by accident either removes files outside the intended directory or leaves a nested copy behind. A dry run (Example 10) shows which level the paths land on.
scp has no such rule: scp -r src host:dir/ always creates dir/src.
The slash rule is the same on macOS. macOS ships its own rsync build, which can be older than the 3.2.7 release used here, so a newer option may be missing. rsync --version shows the installed release, and a current build can be installed with Homebrew.
After the examples, the demo server was stopped with kill $(cat ~/sshd-demo/sshd.pid), and ss -tln | grep -c 2222 printed 0, which confirms nothing listens on port 2222.
Interview points
- A key pair has a private part that never leaves the client and a public part listed in the server's
~/.ssh/authorized_keys. Login proves possession of the private key without sending it. StrictModesmakessshdrefuse key files and directories with loose permissions.~/.sshshould be700andauthorized_keysand private keys600.known_hostsrecords server host keys. A changed host key stops the connection, because it can mean a man-in-the-middle attack or a rebuilt server.accept-newtrusts only servers seen for the first time.ssh host cmdreturns the remote command's exit code and passes stdin and stdout through, so it works inside pipelines. Quote the command to decide which side expands variables and runs pipes.~/.ssh/configblocks setHostName,Port,User, andIdentityFileper name. The first matching value wins, so specific hosts come beforeHost *.ssh -G nameprints the final result.ssh-agentkeeps decrypted keys in memory and answers sign requests throughSSH_AUTH_SOCK. Agent forwarding exposes those keys to admins of the remote host.-L local:host:remotelistens on a local port and sends connections through the server tohost:remote.-Ris the reverse direction and-Dcreates a SOCKS proxy.scptakes-Pfor the port and-rfor directories and always copies whole files.sshtakes-pfor the port.rsync -akeeps permissions, times, and links and skips files whose size and time match.-zcompresses, and--excludeskips patterns.--dry-runpreviews, and--deleteremoves extra destination files. Preview before deleting.- A trailing slash on the rsync source means "the contents of this directory". Without it the directory itself is copied.
Summary
ssh-keygen -t ed25519 -C comment -f pathcreates a key pair. The private file has mode 600.-l -f key.pubprints the fingerprint.- Appending a public key to
~/.ssh/authorized_keyson the server (or runningssh-copy-id) enables key login. Use>>, not>. ssh -p port -i key user@hostconnects. The default port is 22 and the first connection stores the host key in~/.ssh/known_hosts.-o BatchMode=yesmakessshfail instead of prompting.-o StrictHostKeyChecking=accept-newstores new host keys.Permission denied (publickey,...)means no offered key matchedauthorized_keys.ssh -vshows which keys were offered.~/.ssh/configprovides short host names withHostName,Port,User,IdentityFile, andIdentitiesOnly. The alias works forssh,scp, andrsync.ssh host "command"runs a remote command. Its exit code is returned, and-tallocates a terminal.ssh-agentholds unlocked keys.eval "$(ssh-agent -s)"starts it,ssh-addloads a key,ssh-add -llists keys, andssh-add -Dremoves them.ssh -f -N -L lport:host:rport serveropens a background tunnel from a local port tohost:rportas seen from the server.scp [-r] [-P port] source destinationcopies over SSH. A path withhost:is remote.rsync -avz source/ host:dest/copies only changed files, keeps metadata, and compresses data. A second run with no changes lists no files.--dry-run(-n) reports a change without making it, and--itemize-changesshows the reason for each line.--deleteremoves destination files that are absent from the source.rsync a/ b/copies the contents ofaintob.rsync a b/createsb/a.
