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

Scripting an automated backup for an AVM FritzBox router

My home server is set up to backup pretty much every device in my network automatically every day. So I figured why not also backup my AVM FritzBox router. In case it breaks or an update fails and it has to be reset, I can easily restore a backup with all settings, phone book entries, logs etc.

Fortunately, there is a very basic interface for automation, so it is possible to shell script the process of exporting the entire configuration. The result is identical to the manual export in the administration interface to export everything to a backup file. The necessary steps are as follows:

  1. Request a challenge
  2. Log in by responding to the challenge
  3. Extract session ID
  4. Export the backup file
  5. Log out

For sake of simplicity, I will assume that the router is available at https://fritz.box (which it is by default) and that the admin password is supersecret.

A challenge can be requested with the URL https://fritz.box/login_sid.lua:

curl -s -k https://fritz.box/login_sid.lua

The response is a SessionInfo XML, which looks like this:

<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
  <SID>0000000000000000</SID>
  <Challenge>1ma6cy9c</Challenge>
  <BlockTime>0</BlockTime>
  <Rights/>
</SessionInfo>

The challenge is a random string that changes every time the URL is called. Without providing the challenge response, the session ID (SID) will remain empty. To extract the challenge and store it in a variable, simply add a sed statement:

CHALLENGE=`curl -s -k https://fritz.box/login_sid.lua | \
sed 's/.*<Challenge>\([a-z0-9]*\)<.*/\1/'`

So now we have to construct the response and call the URL again. The response is <challenge>-md5(<challenge>-<password>), but it is important to make sure the encoding is UTF-16LE before calculating the hash. Otherwise it will differ and won’t be accepted. This can be done by including iconv:

PASSWORD=supersecret
RESPONSE=$CHALLENGE-`echo -n "$CHALLENGE-$PASSWORD" | iconv -t UTF-16LE | md5`

Call the login URL again, this time with the response:

DATA='response='$RESPONSE'&username=&lp='
curl -s -k --data $DATA https://fritz.box/login_sid.lua

In case you want to use a different user than admin, just provide the username here. The response is again the SessionInfo XML, but SID should be set. It also lists the rights the now logged in user has:

<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
  <SID>962dd5a4b3d75fb2</SID>
  <Challenge>e56b0c6e</Challenge>
  <BlockTime>0</BlockTime>
  <Rights>
    <Name>Dial</Name>
    <Access>2</Access>
    <Name>App</Name>
    <Access>2</Access>
    <Name>HomeAuto</Name>
    <Access>2</Access>
    <Name>BoxAdmin</Name>
    <Access>2</Access>
    <Name>Phone</Name>
    <Access>2</Access>
    <Name>NAS</Name>
    <Access>2</Access>
  </Rights>
</SessionInfo>

In case the SID remains empty, the login failed. If the password is correct, but it still doesn’t work, it may be an encoding issue with the MD5 hash. Keep in mind that the encoding may also depend on the shell used. Use the following example to check your script:

$ echo -n "test" | iconv -t UTF-16LE | md5
c8059e2ec7419f590e79d7f1b774bfe6

To extract the SID and store it in a variable, add the following sed statement:

SID=`curl -s -k --data $DATA https://fritz.box/login_sid.lua | \
sed 's/.*<SID>\([a-z0-9]*\)<.*/\1/'`

This SID can now be used with the call to export all settings for backup. To do this, a form has to be sent to https://fritz.box/cgi-bin/firmwarecfg, including the SID, the desired operation, as well as the password required to restore the backup in this case. I’ll just use the password backup. This is necessary, because the export contains sensitive information, like ISP and SIP accounts, which will be encrypted.

OUT=/tmp/fritzbox.export
BAKPWD=backup
curl -s -k -o $OUT --form sid=$SID --form ImportExportPassword=$BAKPWD \
--form ConfigExport= https://fritz.box/cgi-bin/firmwarecfg

Now the only thing left to do is log out, so the session doesn’t stay open unnecessarily:

DATA='sid='$SID'&logout=1'
curl -s -k --data $DATA https://fritz.box/login_sid.lua

That’s it! The exported file includes all information stored in the FritzBox, so in case anything goes wrong, it allows a complete restore.