A script for rsync backup to an encrypted volume

What's new?

This article is a follow-up to the earlier article Creating an rsync backup script, and the aim of this new article is to describe what needs to be changed in order to work with a backup device which has been encrypted with LUKS. To keep this article short, it will avoid repeating the same advice, so it is recommended you read the earlier article first.

I'm now using Fedora 39 with KDE Plasma 5, but you should be able to get most of the advice on this page to work in other flavours of Linux, possibly requiring a few small tweaks.

Important note: follow the advice on this page at your own risk: make sure you understand the consequences of any commands or actions you intend to take. Read the man pages for any commands or flags with which you're not familiar.

Creating a LUKS-encrypted volume

A Crucial X10 Pro 4TB external SSD is the backup target. For convenience I simply used the KDE Partition Manager to delete the existing (Windows formatted) partitions on the external drive and to create a new ext4 volume using LUKS encryption, choosing a good, long password. But there are many ways to create a LUKS-encrypted volume, so do whatever works best on your system.

I had been hoping to have KDE simply auto-mount the LUKS volume each time I connected it to the USB-C port on my machine, so that I could just use the existing rsync-backup script (described in the earlier article). But this did not work, as KDE would detect the external drive, then detect the LUKS-encrypted volume, then present me with a dialog asking me to enter the decryption password, then fail to do anything useful. Only in about one out of ten attempts did KDE manage to mount the volume successfully. So it was time to resort to the command line.

Mounting the LUKS-encrypted volume

Even though KDE could not automatically mount the encrypted volume, there was nothing wrong with the device nor with the ext4 volume. The volume can be mounted without problem from the command line once you know the UUID of the device. Use this command to see the UUID for all of your connected filesystem devices:

sudo lsblk --fs

If you can't tell which device is the external encrypted drive because you don't know the "filepath" of the device (such as "/dev/sdc1") then use KDE Partition Manager to find the filepath more easily. (Actually, while you're in the partition manager, you can use it to show you the UUID for the LUKS-encrypted partition. In KDE Partition Manager just right-click on the relevant ext4 partition, choose "Properties", then take the value shown next to "UUID", not the value shown next to "Partition UUID".)

Let's say that you've found that the UUID for your encrypted device is 01234567-89ab-cdef-0123-4567890abcde. Then you can open (decrypt) the LUKS volume with this command:

sudo cryptsetup open --type luks UUID=01234567-89ab-cdef-0123-4567890abcde cruc_bkp

The mapping name "cruc_bkp" (Crucial backup) will be used to refer to this device.

You'll be asked for the decryption password, and then the LUKS volume should be open. Assuming you receive no error messages, you can now mount the decrypted volume to a path of your choosing. I'm using Fedora 39, so external drives are mounted under the /run/media/robert path, so this works for me:

sudo mount --mkdir /dev/mapper/cruc_bkp /run/media/robert/Corsair_X10_Pro

The --mkdir option asks Fedora to create the directory path if it does not already exist, and I had trouble when trying to mount without this option.

Assuming you received no error messages, you should now be able to visit the /run/media/robert/Corsair_X10_Pro path and see that it's currently empty. Though the directories seem to have been created with root as the owner and group, so you may not actually be able to read/write anything into that path without using root credentials. With the appropriate credentials, though, you should be able to use rsync to move copies of your files into this path. (The rsync command is more fully described in the earlier article, and also see below for the full script which gives an example of using rsync to move files into this encrypted device.).

Unmounting the LUKS-encrypted volume

Once you've finished backing up your latest files and changes into the external device, it's time to unmount the filesystem, and close the LUKS volume. Unmounting the filesystem can be done with a command like this:

sudo umount /run/media/robert/Corsair_X10_Pro

And closing the LUKS volume can be done with this command:

sudo cryptsetup close /dev/mapper/cruc_bkp

Assuming you received no error messages, the filesystem should be unmounted, and the LUKS volume closed.

The rsync backup script

Typing all of these commands in manually each time you want to backup your important files would be a major pain, so it's far better to shove these commands into a Bash script which you can then run with a single command any time you need it. The script will do the following:

  1. If the filesystem is not already found at the mountpoint, then decrypt the LUKS volume and mount it at the mountpoint.
  2. Run the rsync commands (two in my case).
  3. Offer to unmount the backup volume, and if the user types yes then unmount the filesystem and close the LUKS device.

Here is the script in full:

#!/bin/bash

# Script to backup to LUKS-encrypted Corsair X10 Pro external drive.

# GLOBAL CONSTANTS DEFINED HERE
###############################

# LUKS volume UUID
luks_uuid='01234567-89ab-cdef-0123-4567890abcde'

# Specify the external drive's mount point here
# (DO NOT end mount_point with a forward-slash)
mount_point='/run/media/robert/Corsair_X10_Pro'

# Specify home directory path
# (DO NOT END with a forward-slash)
home_dir='/home/robert'

# Specify the rsync output log filename here
# (use a full path starting with root):
output_log=${home_dir}'/output-rsync.txt'

# Use the year to create a new backup directory each year.
# (That way, if a file is damaged and backed up, at least the
# previous year's backup can be used.)
current_year=`date +%Y`

# Now construct the backup path, specifying the mount point
# followed by the path to our backup directory, finishing with the
# current year. (DO NOT end backup_path with a forward-slash)
backup_path=${mount_point}'/rsync-backup/'${current_year}

# FUNCTION DEFINITIONS
######################

open_luks_volume()
{
	echo "Opening the LUKS volume . . . "
	if ! sudo cryptsetup open --type luks UUID=${luks_uuid} cruc_bkp; then
		echo "An error code was returned by cryptsetup command!"
		return 1
	else
		echo "LUKS volume opened."
		return 0;
	fi
}

mount_filesystem()
{
	echo "Mounting the filesystem . . . "
	if ! sudo mount --mkdir /dev/mapper/cruc_bkp ${mount_point}; then
		echo "An error code was returned by mount command!"
		return 1
	else
		echo "Filesystem mounted at ${mount_point}"
		return 0;
	fi
}

unmount_filesystem()
{
	echo "Dismounting filesystem . . . "
	if ! sudo umount ${mount_point}; then
		echo "An error code was returned by umount command!"
		return 1
	else
		echo "Dismounted successfully."
		return 0;
	fi
}

close_luks_volume()
{
	echo "Closing LUKS volume . . . "
	if ! sudo cryptsetup close /dev/mapper/cruc_bkp; then
		echo "An error was returned by the cryptsetup command!"
		return 1
	else
		echo "LUKS volume closed successfully."
		return 0;
	fi
}

# ACTUAL SCRIPT BEGINS HERE
###########################

echo "#####"
echo ""
# Check whether target volume is mounted, and mount it if not.
if ! mountpoint -q ${mount_point}/; then
	echo "Mounting the Corsair X10 Pro 4TB USB 3.0 drive."
	if ! open_luks_volume; then
		close_luks_volume
		exit 5
	fi
	if ! mount_filesystem; then
		close_luks_volume
		exit 6
	fi
else echo "${mount_point} is already mounted.";
fi
# Target volume **must** be mounted by this point. If not, die screaming.
if ! mountpoint -q ${mount_point}/; then
	echo "Mounting failed! Cannot run backup without target volume!"
	exit 1
fi

echo "Preparing to transfer differences using rsync."

echo "Backup storage directory path is "${backup_path}

echo "Starting backup of ${home_dir} . . . "

# Use rsync to do the backup, and pipe output to tee command (so
# it gets saved to file, AND output to screen)
#
# Note that if you specify just ~ as the source, rsync will
# consider the transfer root directory to be /home, so an include
# rule needs to begin with (for example) /bob/ to actually match
# stuff in the ~ directory. On the other hand, you can set the
# source directory to ~/ (with a forward slash) and then rsync
# will consider the transfer root directory to be ${home_dir} so
# your include/exclude rules only need to begin with / to match
# items in your ~ directory.
#
# Accidentally copying the .gvfs hidden directory caused no end of
# trouble, so I've added an exclude filter here. (The .gvfs
# directory seems to contain a cache of the network drive, so you
# end up copying an infinitely recursive loop of directories. Just
# avoid the .gvfs directory like the plague.)
#
# I used to use --exclude='/.*/' with the rsync command, to skip
# all hidden directories in my home directory, but this is
# dangerous because so many applications store vital user content
# in hidden directories. Note that the 2>&1 part simply instructs
# errors to be written to standard output.
#
# For writing to our ext4 volume we need to use sudo because
# --archive includes options such as --owner which require
# super-user access.

echo "Backing up MyVaultFiles directory using --checksum mode ..."
# First use a dedicated rsync command for vault (VeraCrypt) files
# using --checksum mode (in case the file last-modification
# timestamps have not been updated when the vault file content
# changes).
# You won't need a --checksum mode rsync command like this if you
# don't have files with incorrect timestamps.
sudo rsync --checksum --archive --hard-links --verbose \
	--human-readable --itemize-changes --progress --delete \
	--mkpath \
	${home_dir}/MyVaultFiles/ \
	${backup_path}${home_dir}/MyVaultFiles/ \
	2>&1 | tee ${output_log}

echo "Backing up all other directories using quick-check mode..."
# Now the main rsync command, which backs up everything in the
# home directory tree using the default "quick check" mode (which
# is based on file timestamps).
# Note that we now use the -a flag for the tee command, to append
# to the end of the output log created above.
# DO NOT ADD AN EXCLUSION FOR THE VAULT DIRECTORY HERE, otherwise
# --delete-excluded may delete your vault files from the backup
# location, which is definitely not what you want. (The files are
# tiny in my case, so backing them up twice won't hurt. And the
# timestamp check should skip them anyway.)
sudo rsync --archive --hard-links --verbose --human-readable \
	--itemize-changes --progress --delete --delete-excluded \
	--exclude='/.gvfs/' --exclude='/Examples/' \
	--exclude='/transient-items/' \
	--exclude='/.local/share/Trash/' --exclude='/.cache/' \
	--exclude='/.thumbnails/' --exclude='/.local/share/baloo/' \
	--mkpath \
	${home_dir}/ \
	${backup_path}${home_dir}/ \
	2>&1 | tee -a ${output_log}

# Ask user whether target volume should be unmounted.
echo -n "Do you want to unmount ${mount_point} and close the \
	LUKS device (no)"
read -p ": " answer
answer=${answer,,}  # make lowercase
if [ "$answer" == "y" ] || [ "$answer" == "yes" ]; then
	if ! unmount_filesystem; then
		exit 7
	fi
	if ! close_luks_volume; then
		exit 8
	fi
else echo "Volume remains mounted.";
fi

echo ""
echo "####"

To modify this Bash script to suit you, first you need to change the value of the following variables within the "GLOBAL CONSTANTS" section near the top:

luks_uuid
Set this to the UUID for the device which represents your LUKS-encrypted volume (as described above).
mount_point
Set this to whatever path you want the external drive to be mounted at once it's been decrypted. Do not put a forward slash at the end.
home_dir
The full path to your home directory. Do not put a forward slash at the end.

You can change the values of the other variables too if you see fit, but they should work fine as they are.

You should also carefully check the list of --exclude arguments passed to the rsync command. Exclude any directories you do not want to include in your backup, being careful not to exclude a path which contains something important to you (hidden directories like .local are a particular pain, because they tend to contain a mix of important files unique to you, and random configuration tat that you really don't need to keep).

Warning about rsync and timestamps

If you're using VeraCrypt to manage an encrypted file within your home directory, or if you have any other specialised file or directory whose last-modification timestamp may not be updated even when its content has changed, then make sure to see the warning about rsync and timestamps in the earlier article.

To further reduce the risk of my vault file being skipped, I've now added an initial rsync command to my script which uses --checksum mode to check whether the actual file content (checksum) has changed within the MyVaultFiles directory. This should mean that even if VeraCrypt does not adjust the last-modified timestamp, rsync will still detect that the file has changed and move the latest version to the backup location.

Restoring from backup

See the earlier article for advice about restoring from a backup. Bear in mind that you'll need to connect the external drive, open the LUKS volume, and then mount the filesystem before you can access your backup. So you'll need to either refer to this page, or a copy of your own backup script in order to find the commands needed. And (obviously) make sure you don't forget the password to your encrypted volume.