Backup FreeBSD to an encrypted image on a network share with rsync

While there are many fancy backup tools, often a simple daily rsync may already be sufficient. The advantage of an rsync backup is that it’s file system based, so no special tools are required to browse and access the files in the backup. However, rsync only copies the files, so there is no security measure like encryption. That’s fine locally or within a private network, but sometimes you only have a backup space you can’t trust, like a share on another machine or cloud storage.

Luckily, FreeBSD comes with everything necessary to create an encrypted image to use rsync with. This not only encrypts the backup, but also the copying process, because the encryption is performed before the data is written over the network. In the following, I’ll assume the backup share is mounted locally on /backup. It doesn’t really matter if via SMB, AFP, NFS, or anything else, as long as it can be mounted.

First we need to create an image that is large enough to hold the backup files:

# dd if=/dev/random of=/backup/backup.img bs=1m count=1024
1024+0 records in
1024+0 records out
1073741824 bytes transferred in 6.602198 secs (162633995 bytes/sec)

This will create a 1 GB image, because the block size of 1 MB will be copied 1024 times. Adjust count as necessary. I recommend using /dev/random instead of /dev/zero. While the latter is faster, it will also reveal how much space of the image is in use, since everything not overwritten will still be zero.

Now we can create a vnode for the image, which creates a block device:

# mdconfig -a -t vnode -f /backup/backup.img -u 0

This will create the block device /dev/md0 for the image file. The -u parameter can also be omitted to automatically use the next available index. This block device can be used like any other disk, so it can be encrypted with geli with a key and/or a passphrase. To make scripting easier, I’ll only use a key. Since it’s stored on the machine to create the backup from, a malicious user being able to read it already has access to every data the backup includes. We don’t need to memorize the key, so it can be random:

# dd if=/dev/random of=/usr/local/etc/backup.key bs=4096 count=1

A length of 256 bytes is more than enough, but for the extra paranoia I’ll use 4096 bytes. With the key, the geli layer for the image can be initialized:

# geli init -l 256 -K /backup/backup.key -P /dev/md0

Metadata backup can be found in /var/backups/md0.eli and
can be restored with the following command:

        # geli restore /var/backups/md0.eli /dev/md0

The default algorithm is AES-XTS, which is fine and has hardware support in modern CPUs. The default AES key length is 128 bits, but increasing it to 256 bits with -l 256 can’t hurt. The argument of parameter -K points to the key file and -P disables the passphrase. The following activates the encryption layer for the image device:

# geli attach -k /usr/local/etc/backup.key -p /dev/md0

This time, the argument of -k points to the key file and -p disables the passphrase. As a result, we now have another block device called /dev/md0.eli. It can be used just like the original /dev/md0, but all data passing through it will be encrypted and decrypted, before written to and read from /dev/md0, and therefore the image file. At this point we have encrypted access to the image file like a normal but still empty disk. Every disk needs a file system, so does our image:

# newfs /dev/md0.eli
/dev/md0.eli: 1024.0MB (2097144 sectors) block size 32768, fragment size 4096
        using 4 cylinder groups of 256.00MB, 8192 blks, 32768 inodes.
super-block backups (for fsck_ffs -b #) at:
 192, 524480, 1048768, 1573056

It is also possible to create a partition table with multiple partitions and file systems, but for the sake of simplicity I’ll just use the entire image with UFS2. Now it’s time to mount:

# mount /dev/md0.eli /mnt

That’s pretty much it! Now simply use rsync or and even cp to backup files. Here is an example for rsync:

# rsync -a --delete --exclude '/mnt' --exclude '/dev' \
  --exclude '/backup' / /mnt/

This creates a full recursive backup (-a) of / to /mnt and deletes files in the destination directory that have been deleted in the source directory since the last backup (–delete). To avoid loops and errors when trying to access the devices in /dev, some excludes should be added.

After the backup is finished, the image file can be unmounted, the geli layer detached and the vnode destroyed:

# umount /dev/md0.eli
# geli detach /dev/md0
# mdconfig -d -u 0

The steps necessary for a backup script, which can be scheduled via cron, are creating the vnode, attaching the geli layer, mounting, performing the backup, and cleaning up nicely:

#!/bin/sh
mdconfig -a -t vnode -f /backup/backup.img -u 0
geli attach -k /usr/local/etc/backup.key -p /dev/md0
mount /dev/md0.eli /mnt
rsync -a --delete --exclude '/mnt' --exclude '/dev' --exclude '/backup' / /mnt/
umount /dev/md0.eli
geli detach /dev/md0
mdconfig -d -u 0