Remote backup with Rsync and Wireguard

I have recently set up a remote backup system from a Linux computer to a Raspberry Pi connected to the internet in another place. This update is performed with rsync over SSH, using a Wireguard VPN tunnel so both computers behave as if they were on the same local network.

Here I’ll describe all the steps I had to follow to get it working!

Bill of Materials

The hardware BoM is limited to the following:

An OS on the Raspberry Pi

Of course an Operating System is needed to bring the Raspberry Pi to life. As it’s a backup system, its tasks won’t be very various. The Wireguard VPN tunnel will always be open. Once a day, the other computer will connect to the tunnel, ask the Raspberry to mount the external disk then push a backup with rsync.

Even if the plan is to keep it always powered on, there is no UPS (Uninterruptible power supply) to keep the power safe. Having a read-only filesystem can be a good addition. I want to make it as easy as possible, so I don’t want to make a custom OS with Yocto or Buildroot. My choice goes then to Alpine Linux.

“Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.” as they say on the website. Even better, on the Raspberry Pi it’s booting the OS from the SD card, then the rootfs is held in RAM so nothing is persistent on it. It’s perfectly power shortage proof!

Installing Alpine

First, make a FAT32 partition on the SD card with the tool you prefer. I personnaly use GParted.

Then download the right installation archive from the Downloads page of Alpine. Extract the contents of the archive to the freshly created partition of the SD card. If you don’t want to have to connect the Pi to a display and keyboard for its setup, you can use the headless bootstrap overlay by putting the headless.apkovl.tar.gz file at the root of the SD card too. Magic can happen, put the SD card into the Raspberry Pi, connect it to the wired network and power it on!

Retrieve the IP of the Pi as you can (from your router’s config panel for instance) and connect to it via SSH with the root user:

ssh root@<ip>

Now you can do the basic setup of you system with the setup-alpine tool.

I also recommend to enable the community repositories of Alpine, as explained here. You can also configure SSHD to refuse password authentication and enable public key authentication.

Configure SSH

First, on the computer side, you have to create a SSH key and copy it to the Raspberry Pi with the following commands on the computer side:

ssh-keygen -t ed25519
ssh-copy-id root@<ip_of_the_pi>

On the Raspberry Pi side, configuration needs to be changed in the file /etc/sshd/sshd_config. As the rootfs of Alpine won’t be persistent, you also have to specify that the keys are stored somewhere else than the default.

PubkeyAuthentication yes
PermitRootLogin yes
PasswordAuthentication no
AuthorizedKeysFile	/etc/ssh/root_authorized_keys

Then copy the SSH key to this new path and restart SSHD:

mkdir /etc/ssh/root_authorized_keys
mv /root/.ssh/id_ed25519 /etc/ssh/root_authorized_keys
rc-service sshd restart

Now when connecting via SSH, you won’t get asked a password.

Install needed packets

Our last initial step is to install all the tools that we will need now. Let’s do this:

apk update
apk add wireguard-tools-wg-quick wireguard-tools-openrc iptables ip6tables ufw ddclient rsync nano

And remember: as nothing is persistent by default on Alpine, don’t forget to call lbu commit -d to commit the pending changes.

Dynamic IP handling with ddclient

As you can imagine, the Raspberry Pi is not in a data center. It’s plugged to a basic set-top-box over ethernet. And this set-top-box doesn’t provide a static IP address. To be able to always join the Raspberry Pi from the outside, I’ll use a domain name redirecting to the right IP address: it’s called a Dynamic DNS.

Thanks to a properly configured ddclient, the IP address linked to my domain will always be up to date. I can’t really make a full tutorial on how to set this up, but here is a general flow:

  • Buy a domain name at a registar
  • Enable dynamic DNS on this domain
  • Create authentication credentials to allow the target update of the domain
  • Configure ddclient
  • Open port 51820 of your router to route the VPN traffic from the internet to the Raspberry Pi

Configure ddclient

We installed ddclient in the previous steps. Now it’s time to configure it. You can edit the configuration file in /etc/ddclient/ddclient.conf. Here are the interesting lines to change or uncomment:

use=web

...

##
## Your registar
##
protocol=<protocol>,
server=<server>
use=web

login=<login you created>
password='<password you created>'
<your domain name>

After saving the file, enable the service at boot:

rc-service ddclient start
rc-update add ddclient default

And of course, lbu commit -d.

The VPN tunnel

As the Raspberry Pi is not connected to the same network as the computer to backup, I needs to be reachable on the internet. I didn’t want to expose directly its the SSH port, so I have set up a VPN tunnel between the two systems. This way, the Raspberry Pi and the computer can be virtually connected to the same private network.

I chose to use Wireguard because it’s as powerful as it’s easy to configure.

Raspberry Pi side

From the point of view of the VPN tunnel, the Raspberry Pi is the server. First, we need to configure cryptographic keys for it.

wg genkey | tee /etc/wireguard/server.privatekey | wg pubkey > /etc/wireguard/server.publickey

We just created a private key in /etc/wireguard and derived its public key to the same location. Now let’s configure the Wireguard interface itself:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server/privatekey>
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;iptables -D FORWARD -o %i -j ACCEPT

[Peer]
PublicKey = <computer's key that we'll create at next step>
AllowedIPs = 10.0.0.2/32

Save this to /etc/wireguard/wg0.conf. With this configuration, the Raspberry Pi will have IP 10.0.0.1 and the computer 10.0.0.2. Only the traffic related to the exchanges between both devices will flow through the tunnel.

Computer side

Next step is to configure the computer side. Supposing you do this on an Ubuntu computer, install Wireguard tools:

sudo apt-get install wireguard-tools

Create keys:

wg genkey | tee /etc/wireguard/client.privatekey | wg pubkey > /etc/wireguard/client.publickey

And create the Wireguard configuration in /etc/wireguard/wg0.conf with the following contents:

[Interface]
Address = 10.0.0.2/32
PrivateKey = <contents of /etc/wireguard/client.privatekey>
DNS = 1.1.1.1

[Peer]
PublicKey = <private key of the Raspberry Pi>
Endpoint = <public IP or domain name of the Raspberry Pi>:51820
AllowedIPs = 10.0.0.1/24

Now, you can finalize the Wireguard configuration file on the Raspberry Pi with the computer’s public key. It will look as follows:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.privatekey>
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;iptables -D FORWARD -o %i -j ACCEPT

[Peer]
PublicKey = <contents of /etc/wireguard/client.privatekey on the computer>
AllowedIPs = 10.0.0.2/32

Save the file, and enable the tunnel at boot:

ln -s /etc/init.d/wg-quick /etc/init.d/wg-quick.wg0
rc-update add wg-quick.wg0
rc-service wg-quick.wg0 start

A firewall

Using a firewall is always a good practice. Even if the ports of your Linux system are not supposed to be open, it’s safer to block traffic through them. The only services used on our system are the SSH and Wireguard protocols.

UFW

UFW (Uncomplicated FireWall) provides an easy way to manage the firewall rules of a Linux system. Here is how you configure it:

ufw default deny incoming    # Block all incoming traffic
ufw default allow outgoing   # Allow all outgoing traffic
ufw allow SSH                # Allow the SSH protocol
ufw allow 51820/udp          # Allow the Wireguard protocol
ufw enable                   # Enable the firewall
rc-update add ufw            # Start the firewall at boot

Aaaaand…. lbu commit -d.

Testing the tunnel

Now you can reboot the Raspberry Pi. It should start automatically the Wireguard server at boot and let incoming traffic come to it.

From the computer, open the link with

sudo wg-quick up/down wg0

You can verify that the target is reachable by pinging it with

ping 10.0.0.1

And see this kind of output if everything goes well:

PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=6.53 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=1.59 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=1.81 ms
^C
--- 10.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.589/3.309/6.533/2.280 ms

Close the VPN tunnel with

sudo wg-quick down wg0

Data backup

Remember, we are there to backup the computer to the Raspberry Pi on an external drive. The backup flow will be generally:

  1. Open the VPN link from the computer
  2. Connect to the Raspberry Pi via SSH
  3. Mount the external drive
  4. Do the backup
  5. Unmount the drive
  6. Close the tunnel

A script makes it easier!

#!/bin/sh

#
#	Rsync backup on a remote Raspberry-Pi over a Wireguard Tunnel
#	BalthD - 2025
#

# Return success by default
RETURN_CODE=0

# Command to use to connect over SSH: using key file, no server authenticity check
SSH_COMMAND="ssh -o StrictHostKeyChecking=no

# Start Wireguard tunnel
echo "Opening Wireguard tunnel..."
sudo wg-quick up wg0 > /dev/null 2>&1

# Check that the target server is reachable
if  nc -w 5 -z 10.0.0.1 22 2>/dev/null; then
	echo "Server is reachable"

	# Mount disk on the remote target
	echo "Mounting backup disk"
	if $SSH_COMMAND root@10.0.0.1 "mkdir -p /media/External & mount UUID=<disk UUID> /media/External"; then

		# Do the actual backup
		echo "Doing the backup"
		if sudo rsync -av --update --delete --no-links --info=progress2 -e "${SSH_COMMAND}" "<directory to backup>" root@10.0.0.1:"/media/External/Backup"; then
			echo "Backup succeedded"
		else
			echo "Backup failed"
			RETURN_CODE=1
		fi

		# Unmount disk on the target
		echo "Unmounting the backup disk"
		if $SSH_COMMAND root@10.0.0.1 "umount /media/External"; then
			echo "Backup disk unmounted"
		else
			echo "Failed to unmount the backup disk"
			RETURN_CODE=1
		fi
	else
		echo "Unable to mount the backup disk"
		RETURN_CODE=1
	fi
else
	echo "Can't reach the server"
	RETURN_CODE=1
fi

# Stop the Wiregard tunnel
echo "Closing the Wireguard tunnel"
sudo wg-quick down wg0 > /dev/null 2>&1

exit $RETURN_CODE

Replace <disk UUID> with the UUID of the partition where you want to save the backup. You can get it with the command sudo blkid.

Of course, replace <directory to backup> with the path to the directory you want to backup.

Don’t forget to make the script executable with chmod +x.

Use a CRON

The last step is to make a CRON to call this script automatically. Nothing easier: if you want to backup your data every day, just save the script above in /etc/cron.daily on the computer, and the screen will be called every day at the same time. Your computer has to be powered on at the time of the CRON of course.

Conclusion

To conclude, we’ve setup something that looks like a network backup disk. Our precious data can be saved in another place automatically each night. In addition, the flow is twice secured as the VPN encrypts the already-encrypted traffic of the SSH protocol.

It’s important to remember the 3 – 2 – 1 rule of the backup:

  • Make 3 copies of your data
  • 2 backups must be stored on different mediums or devices
  • At least 1 backup has to be stored offline or in another place of the original

I now fulfill all those rules: one original data, one copy on a hard drive and one copy now off-site!

Loading