Home / Learn Linux / Project 3
Backups between two machines over SSH
Two machines, key-only SSH and an rsync backup you can run with one command.
- Project
- 3 of 5
- Time
- About 60 minutes
- Uses
- Lessons 18 to 21
On this page
- What you'll build
- Step 0: two machines
- Step 1: the backup account (on vault)
- Step 2: a key just for backups (on your machine)
- Step 3: install the key, locked down (on vault)
- Step 4: a shortcut, and the first login
- Step 5: something worth backing up
- Step 6: the first backup
- Step 7: one command
- Step 8: a snapshot for every day
- Step 9: prove it with a restore
- Step 10: what's on vault?
- What you built
- Take it further
What you'll build
A backup that runs with one command, from your machine to a separate backup server, over SSH:
- the backup server has its own locked-down
backupaccount and storage directory - your machine logs in with a dedicated key that can't be used for anything but running commands
rsyncsends only what changed, so later backups take seconds- every day gets its own complete snapshot, but unchanged files take no extra space
- and you'll prove it works by restoring a file, which is the only test of a backup that counts
You'll use users (lesson 13), permissions (14), SSH and keys (19), rsync and checksums (20), and hard links (11).
Step 0: two machines
The backup server is called vault throughout. Choose one way to get it:
- A second VM (recommended). On your computer, run
multipass launch 26.04 --name vault --memory 1G --disk 8G, thenmultipass listto find its address. In yourlinuxVM, give that address the namevault:echo "192.168.64.7 vault" | sudo tee -a /etc/hosts, using vault's real address. Run the "On vault" steps below inside it withmultipass shell vault. - One VM, if your computer is short on memory. The backup server is just another user on the same VM. Point the name at the machine itself:
echo "127.0.0.1 vault" | sudo tee -a /etc/hosts. Everything works the same, only the address differs.
The output on this page comes from the one-VM way, so prompts show linux where yours might show vault.
$ echo "127.0.0.1 vault" | sudo tee -a /etc/hosts 127.0.0.1 vault $ getent hosts vault 127.0.0.1 vault
Step 1: the backup account (on vault)
On vault, create a user called backup with no password (it will only ever log in with a key), and a storage directory /srv/backups that only backup can read or write.
Hint
adduser --disabled-password, then mkdir, chown backup:backup and chmod 700.
Solution
$ sudo adduser --disabled-password --comment "Backup receiver" backup $ sudo mkdir /srv/backups $ sudo chown backup:backup /srv/backups $ sudo chmod 700 /srv/backups $ sudo ls -ld /srv/backups drwx------ 2 backup backup 4096 Oct 21 15:51 /srv/backups
Step 2: a key just for backups (on your machine)
Backups should run without anyone typing a passphrase, so they need a key without one. That makes the key valuable to steal, so don't reuse your own: make a dedicated key called ~/.ssh/backup_key, and limit what it can do in the next step.
Solution
$ ssh-keygen -t ed25519 -f ~/.ssh/backup_key -N "" -C "backup from linux" -q $ ls -l ~/.ssh/backup_key* -rw------- 1 ubuntu ubuntu 411 Oct 21 15:51 /home/ubuntu/.ssh/backup_key -rw-r--r-- 1 ubuntu ubuntu 99 Oct 21 15:51 /home/ubuntu/.ssh/backup_key.pub $ cat ~/.ssh/backup_key.pub ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICbFsnvxLzNXQJBJfEvBiCVUWZYC4R7eNVGVsIUVr8sj backup from linux
-q keeps ssh-keygen quiet.
Step 3: install the key, locked down (on vault)
Put the public key into backup's ~/.ssh/authorized_keys on vault, with the right owner and permissions. Then lock it down: putting restrict in front of a key in authorized_keys turns off everything SSH can do besides running commands, like port forwarding and terminals. rsync still works, because it only runs a command.
Hint
On a second VM, copy the .pub line across by pasting it (it's one line). On one VM, cat it into sudo tee. Lesson 19 has the permissions: 700 for .ssh, 600 for the file, owned by backup.
Solution
$ sudo mkdir -p /home/backup/.ssh $ echo "restrict $(cat ~/.ssh/backup_key.pub)" | sudo tee /home/backup/.ssh/authorized_keys restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICbFsnvxLzNXQJBJfEvBiCVUWZYC4R7eNVGVsIUVr8sj backup from linux $ sudo chown -R backup:backup /home/backup/.ssh $ sudo chmod 700 /home/backup/.ssh $ sudo chmod 600 /home/backup/.ssh/authorized_keys
Step 4: a shortcut, and the first login
Add a vault entry to your ~/.ssh/config that logs in as backup with backup_key. Then test it by running whoami on vault. It's the first connection to the name vault, so SSH asks you to confirm the host key (lesson 19).
Solution
$ printf "Host vault\n HostName vault\n User backup\n IdentityFile ~/.ssh/backup_key\n IdentitiesOnly yes\n\n" >> ~/.ssh/config $ ssh vault whoami The authenticity of host 'vault (127.0.0.1)' can't be established. ED25519 key fingerprint is: SHA256:ZVOScnETK5a0XrLkGdWrXwgnybL4hn7wEVwfoimHCa8 This host key is known by the following other names/addresses: ~/.ssh/known_hosts:1: [hashed name] Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'vault' (ED25519) to the list of known hosts. backup@vault: Permission denied (publickey).
If you hardened SSH in lesson 19 on this same machine, the login itself fails, and the server's log says why:
$ sudo journalctl -u ssh --since "1 min ago" --no-pager | grep -i "not allowed" | tail -n 1
Oct 21 15:51:33 linux sshd-session[288878]: User backup from 127.0.0.1 not allowed because not listed in AllowUsersAllowUsers from lesson 19 only lets ubuntu and ravi in. (On a fresh second VM you won't have this problem.) Add backup to the list, check the config, reload, and try again:
$ sudo sed -i 's/^AllowUsers .*/AllowUsers ubuntu ravi backup/' /etc/ssh/sshd_config.d/10-hardening.conf $ sudo sshd -t && sudo systemctl reload ssh $ ssh vault whoami backup
Check: the key really is restricted
restrict means no interactive shell for this key. A normal login now gets no terminal:
$ ssh vault 'tty'
not a ttynot a tty confirms it: commands run, but nobody can sit at a terminal with this key.
Step 5: something worth backing up
Make ~/important with a few files of different kinds, including a big one, so you can see rsync skip it the second time.
Solution
$ mkdir -p ~/important/{docs,photos,code} $ echo "Quarterly report draft" > ~/important/docs/report.txt $ echo "Rent: 15000" > ~/important/docs/budget.csv $ head -c 5000000 /dev/urandom > ~/important/photos/trip.jpg $ printf 'print("hello")\n' > ~/important/code/app.py $ du -sh ~/important 4.8M /home/ubuntu/important $ find ~/important -type f | wc -l 4
Step 6: the first backup
Send ~/important to /srv/backups/linux/latest/ on vault with rsync, keeping permissions and times, compressed, and as an exact mirror. Then check from vault's side what arrived.
Hint
rsync -az --delete source/ vault:/srv/backups/linux/latest/. Remote directories that don't exist yet need --mkpath. And check the trailing slash on the source (lesson 20).
Solution
$ rsync -az --delete --mkpath --stats ~/important/ vault:/srv/backups/linux/latest/ | grep -E "Number of regular files transferred|Total transferred" Number of regular files transferred: 4 Total transferred file size: 5,000,050 bytes $ ssh vault 'find /srv/backups/linux/latest -type f' /srv/backups/linux/latest/photos/trip.jpg /srv/backups/linux/latest/code/app.py /srv/backups/linux/latest/docs/budget.csv /srv/backups/linux/latest/docs/report.txt
Run exactly the same command again. Nothing changed, so nothing is sent:
$ rsync -az --delete --mkpath --stats ~/important/ vault:/srv/backups/linux/latest/ | grep -E "Number of regular files transferred|Total transferred"
Number of regular files transferred: 0
Total transferred file size: 0 bytesStep 7: one command
Put the backup into a small script, ~/bin/backup.sh, that also writes a line to ~/backup.log each time it runs. Lesson 27 explains scripts properly; for now, here's all you need:
- the first line,
#!/bin/bash, says which program runs the script set -estops the script at the first command that fails, so a broken backup never reports success- the script must be executable:
chmod +x(lesson 14)
Solution
$ mkdir -p ~/bin $ cat > ~/bin/backup.sh << 'EOF' > #!/bin/bash > set -e > rsync -az --delete --mkpath ~/important/ vault:/srv/backups/linux/latest/ > echo "$(date '+%F %T') backup ok" >> ~/backup.log > EOF $ chmod +x ~/bin/backup.sh $ ~/bin/backup.sh $ cat ~/backup.log 2026-10-21 15:51:45 backup ok
The quotes in << 'EOF' stop the shell from running $(date ...) while writing the file, so the script itself gets the $(...) and works out the date each time it runs.
Step 8: a snapshot for every day
A mirror only has today. If a file was damaged a week ago and you only notice now, the mirror has faithfully copied the damage. You want a copy for every day, without storing everything again each day.
rsync --link-dest does exactly that. Point it at yesterday's snapshot: files that haven't changed become hard links (lesson 11) to yesterday's copies, so they take no extra space, and only changed files are stored again. Every day's folder still looks like a complete backup.
Make two dated snapshots, changing one small file in between:
$ ssh vault 'rm -rf /srv/backups/linux/2026-10-20 /srv/backups/linux/2026-10-21' $ rsync -a ~/important/ vault:/srv/backups/linux/2026-10-20/ $ echo "Rent: 16000" > ~/important/docs/budget.csv $ rsync -a --link-dest=/srv/backups/linux/2026-10-20 ~/important/ vault:/srv/backups/linux/2026-10-21/
Now look at the big photo in both snapshots. Same inode number, link count 2: it's stored once. The budget file changed, so it has two different inodes:
$ ssh vault 'ls -li /srv/backups/linux/2026-1*/photos/trip.jpg /srv/backups/linux/2026-1*/docs/budget.csv'
535404 -rw-rw-r-- 1 backup backup 12 Oct 21 15:51 /srv/backups/linux/2026-10-20/docs/budget.csv
535406 -rw-rw-r-- 2 backup backup 5000000 Oct 21 15:51 /srv/backups/linux/2026-10-20/photos/trip.jpg
535411 -rw-rw-r-- 1 backup backup 12 Oct 21 15:51 /srv/backups/linux/2026-10-21/docs/budget.csv
535406 -rw-rw-r-- 2 backup backup 5000000 Oct 21 15:51 /srv/backups/linux/2026-10-21/photos/trip.jpgAnd the disk space: du counts each stored file only once, so the second snapshot costs almost nothing:
$ ssh vault 'du -sh /srv/backups/linux/2026-10-20 /srv/backups/linux/2026-10-21'
4.8M /srv/backups/linux/2026-10-20
20K /srv/backups/linux/2026-10-21Two full-looking backups for the space of one. A real script would use $(date +%F) for today's folder and yesterday's for --link-dest.
Step 9: prove it with a restore
A backup you've never restored from is a hope, not a backup. Simulate losing a file: delete report.txt, restore it from the latest snapshot into a separate directory, and prove it's byte-for-byte the original by comparing checksums with the copy on vault.
Solution
$ rm ~/important/docs/report.txt $ ls ~/important/docs budget.csv $ rsync -a vault:/srv/backups/linux/latest/docs/report.txt ~/restore-test/ $ cat ~/restore-test/report.txt Quarterly report draft $ sha256sum ~/restore-test/report.txt 870f3488796646fb0279e12ba464f51e2b9f338e1852e1bdd910d96ebe775276 /home/ubuntu/restore-test/report.txt $ ssh vault 'sha256sum /srv/backups/linux/latest/docs/report.txt' 870f3488796646fb0279e12ba464f51e2b9f338e1852e1bdd910d96ebe775276 /srv/backups/linux/latest/docs/report.txt $ cp ~/restore-test/report.txt ~/important/docs/
The two checksums match. And because the next backup.sh would mirror your deletion into latest, the dated snapshots are what save you if you notice a loss late.
Step 10: what's on vault?
Show everything stored for this machine, two levels deep, and the total space used.
Solution
$ ssh vault 'cd /srv/backups/linux && find . -maxdepth 2 -type d | sort && du -sh .'
.
./2026-10-20
./2026-10-20/code
./2026-10-20/docs
./2026-10-20/photos
./2026-10-21
./2026-10-21/code
./2026-10-21/docs
./2026-10-21/photos
./latest
./latest/code
./latest/docs
./latest/photos
9.7M .What you built
| Piece | Where | Why |
|---|---|---|
backup user, /srv/backups 700 | vault | Backups belong to one account nobody logs into |
~/.ssh/backup_key with restrict | both | Unattended logins that can only run commands |
Host vault in ~/.ssh/config | your machine | One short name for address, user and key |
~/bin/backup.sh | your machine | The whole backup in one command, with a log |
--link-dest snapshots | vault | A full copy per day, space only for changes |
Take it further
- Run
backup.shevery night with cron (lesson 24), and check~/backup.login the morning. - Turn Step 8 into the script: today's folder with
$(date +%F), yesterday's with$(date -d yesterday +%F)in--link-dest. The Bash lessons (27 to 29) give you everything you need. - Delete snapshots older than 30 days on vault with
find /srv/backups/linux -maxdepth 1 -type d -mtime +30(look first, then add-exec rm -rf {} +). - Look at
resticorborg, which add encryption and deduplication on top of the same idea.