Skip to content
elephantoo

SSH, keys, scp & rsync

Lesson 28 of 31 18 min read

Connect securely, set up key-based login, use ~/.ssh/config and tunnels, and copy files with scp and rsync.


SSH (Secure Shell) is how you work on remote Linux machines: cloud servers, Raspberry Pis, the build box at work. It gives you an encrypted terminal session, secure file transfer, and tunnels to services that aren't exposed to the internet. In this lesson you'll connect to servers, set up key-based login, simplify everything with ~/.ssh/config, forward ports, and copy files with scp and rsync.

Connecting#

Terminal
ssh ada@203.0.113.10            # user@host
ssh ada@server.example.com
ssh -p 2222 ada@server.example.com   # non-standard port
ssh ada@server 'uptime && df -h /'   # run one command and return

The client (ssh) comes with every Linux distribution and macOS, and with Windows 10/11. To accept connections, a machine needs the server (sshd): sudo apt install openssh-server on Ubuntu/Debian or sudo dnf install openssh-server on Fedora, then sudo systemctl enable --now ssh (the unit is sshd on Fedora/RHEL).

The first time you connect, SSH shows the server's host key fingerprint:

Output
The authenticity of host 'server.example.com (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:4fZ1Q2hHkqK2b7cN0yQm3T8pW9xV6sR5aE1dF7gH2jK.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'server.example.com' (ED25519) to the list of known hosts.

Answering yes stores it in ~/.ssh/known_hosts. If it ever changes, SSH refuses to connect with a big WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!. That means either the server was reinstalled, or someone is intercepting the connection. If you know it's a reinstall, remove the old entry with ssh-keygen -R server.example.com.

Leave a session with exit or Ctrl+D. If a connection hangs, type Enter ~ . to kill it.

Key-based authentication#

Passwords can be guessed and brute-forced. SSH keys are far stronger and more convenient. A key pair has two parts:

  • a private key (~/.ssh/id_ed25519) that stays on your machine and is secret;
  • a public key (~/.ssh/id_ed25519.pub) that you put on every server you want to log in to.

1. Generate a key pair

Terminal
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keygen -t ed25519 -C "ada@laptop" -f ~/.ssh/id_ed25519 -N ""
ls -l ~/.ssh
cat ~/.ssh/id_ed25519.pub | cut -c1-40
Output
Generating public/private ed25519 key pair.
Your identification has been saved in /home/ada/.ssh/id_ed25519
Your public key has been saved in /home/ada/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:p94suxgetnH1lVejUCIebr5rlyEeKFW67pEz7/Rjxzo ada@laptop
The key's randomart image is:
+--[ED25519 256]--+
|         o . .   |
|        o.o o    |
|        o+ .   ..|
|       oo   . ..o|
|      . S.o  .o .|
|     . o.*.o . . |
|      B=+oo =    |
|     o XB=oE o   |
|      =.*B=o=    |
+----[SHA256]-----+
total 8
-rw------- 1 ada ada 399 Oct  1 11:09 id_ed25519
-rw-r--r-- 1 ada ada  92 Oct  1 11:09 id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINqM

(-N "" creates the key without a passphrase so this example runs non-interactively. For your real key, leave off -N and choose a passphrase when prompted; ssh-agent means you'll only type it once per session.) Ed25519 is the modern default: short, fast and secure. Use -t rsa -b 4096 only for very old systems.

Notice the permissions: the private key is 600 (only you can read it). SSH refuses to use private keys that others can read.

2. Copy the public key to the server

Terminal
ssh-copy-id ada@server.example.com

This appends your public key to ~/.ssh/authorized_keys on the server (and fixes its permissions). If ssh-copy-id isn't available, do it by hand:

Terminal
cat ~/.ssh/id_ed25519.pub | ssh ada@server 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'

Cloud providers (AWS, DigitalOcean, Hetzner...) let you paste your public key when you create a server, so it's there from the first boot.

3. Log in, with no password

Terminal
ssh ada@server.example.com

ssh-agent: type your passphrase once

Terminal
eval "$(ssh-agent -s)"      # start an agent (desktop sessions usually have one already)
ssh-add ~/.ssh/id_ed25519   # unlock the key once
ssh-add -l                  # list loaded keys

~/.ssh/config: stop typing long commands#

Create ~/.ssh/config (mode 600) to give hosts short names and default options:

~/.ssh/config
Host web1
    HostName 203.0.113.10
    User ada
    IdentityFile ~/.ssh/id_ed25519

Host db
    HostName 10.0.1.20
    User ada
    ProxyJump web1            # reach a private server through web1 (a "bastion")

Host github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_github

Host *
    ServerAliveInterval 60    # keep idle connections from timing out
    AddKeysToAgent yes

Now ssh web1 is all you type, and ssh db hops through web1 automatically. scp, rsync, git and VS Code Remote-SSH all use these aliases too. You can check what SSH will actually use for a host with ssh -G web1:

Terminal
mkdir -p ~/.ssh && cat > ~/.ssh/config <<'EOF'
Host web1
    HostName 203.0.113.10
    User ada
    Port 2222
EOF
chmod 600 ~/.ssh/config
ssh -F ~/.ssh/config -G web1 | grep -E '^(hostname|user|port) '   # -F: use this config file (normally automatic)
Output
user ada
hostname 203.0.113.10
port 2222

Copying files: scp#

scp copies files over SSH, with the same syntax as cp plus host: prefixes:

Terminal
scp report.pdf web1:/tmp/                 # local -> remote
scp web1:/var/log/nginx/access.log .      # remote -> local
scp -r ./site web1:/var/www/              # a whole directory
scp -P 2222 file.txt ada@host:~/          # note: capital -P for the port

Syncing files: rsync#

rsync is smarter: it only transfers what changed, can resume, preserves permissions and timestamps, and can delete files that disappeared from the source. It's the standard tool for deployments, backups and mirroring. Let's try it locally (it works the same over SSH):

Terminal
mkdir -p site/css && echo "<h1>Hi</h1>" > site/index.html && echo "body{}" > site/css/main.css
rsync -av site/ backup/
echo "<h1>Hello</h1>" > site/index.html
rm site/css/main.css
rsync -av --delete site/ backup/
Output
sending incremental file list
created directory backup
./
index.html
css/
css/main.css

sent 249 bytes  received 98 bytes  694.00 bytes/sec
total size is 19  speedup is 0.05
sending incremental file list
index.html
deleting css/main.css

sent 165 bytes  received 59 bytes  448.00 bytes/sec
total size is 15  speedup is 0.07

The second run only copied the changed file, and --delete removed the file that no longer exists in the source. Over the network:

Terminal
rsync -avz --progress ./site/ web1:/var/www/site/       # push
rsync -avz web1:/var/backups/ ./server-backups/         # pull
rsync -avz --delete --exclude '.git' --exclude 'node_modules' ./app/ web1:/srv/app/
rsync -avzn --delete ./site/ web1:/var/www/site/        # -n = dry run: show what WOULD happen
OptionMeaning
-aarchive: recursive, and preserve permissions, times, symlinks, owner/group
-vverbose
-zcompress during transfer
-n / --dry-runshow what would be done, without doing it
--deletedelete files in the destination that aren't in the source
--exclude PATTERNskip matching files
-P--progress plus --partial (resume big files)

⚠️ The trailing slash matters. rsync -a site/ dest/ copies the contents of site into dest. rsync -a site dest/ creates dest/site/. Combined with --delete, getting this wrong can wipe a directory, so always do a dry run (-n) first.

Port forwarding (SSH tunnels)#

Local forwarding lets you reach a service that only listens on the server's localhost, such as a database, without opening it to the internet:

Terminal
ssh -L 5433:localhost:5432 web1       # then connect your DB client to localhost:5433
ssh -N -L 8080:10.0.1.30:80 web1      # -N: no shell, just the tunnel; reach a private web server

Remote forwarding does the reverse, exposing a port on your machine to the server: ssh -R 9000:localhost:3000 web1. Dynamic forwarding creates a SOCKS proxy: ssh -D 1080 web1.

Keeping long jobs alive#

If your connection drops, processes started in the session get SIGHUP and usually die. For long-running work, start tmux on the server first (tmux new -s deploy, detach with Ctrl+B D, reattach with tmux attach -t deploy). See the processes lesson for nohup and disown.

Server-side basics: sshd_config#

The SSH server is configured in /etc/ssh/sshd_config and drop-in files in /etc/ssh/sshd_config.d/*.conf (Ubuntu and Fedora both use the drop-in directory). Once your key login works, the two most important hardening settings are:

/etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
PermitRootLogin no

Always test the config and keep your current session open while you check that a new login still works:

Terminal
sudo sshd -t && sudo systemctl reload ssh     # 'sshd' on Fedora/RHEL

More hardening (fail2ban, AllowUsers, changing ports) is covered in the security lesson.

Common mistakes#

  • Wrong permissions on ~/.ssh. SSH silently ignores keys if ~/.ssh is not 700, authorized_keys not 600, or your home directory is group-writable. Run ssh -v host to see why a key was rejected.
  • Sharing or copying private keys between people or laptops. Generate one key per person per device.
  • No passphrase on a key that grants production access.
  • Disabling password login before testing key login, and locking yourself out. Keep a second session open.
  • Blindly typing yes to a changed host key warning.
  • The rsync trailing-slash mix-up with --delete. Dry-run first.
  • scp -p 2222: scp's port flag is capital -P (lowercase -p preserves times).

What's next#

Now that SSH is the front door to your servers, the next step is controlling what else can get in: firewalls with ufw (and firewalld on Fedora/RHEL).

Check your understanding

Quick quiz

0/3 answered
  1. 1.With key-based SSH login, which file must stay secret?

  2. 2.What's the difference between rsync -a src/ dest/ and rsync -a src dest/?

  3. 3.What does ssh -L 5433:localhost:5432 ada@db.example.com do?

Finished reading?

Mark this lesson complete to track your progress.