Skip to content

Instantly share code, notes, and snippets.

@skoenig
Last active April 11, 2024 13:38
Show Gist options
  • Save skoenig/c19f707e02a274f6371697163ee18b9f to your computer and use it in GitHub Desktop.
Save skoenig/c19f707e02a274f6371697163ee18b9f to your computer and use it in GitHub Desktop.
extshot - Ensure automated encrypted backups and long-term storage for Linux and macOS.

extshot

Whoo backups... exiting, right? No. But it has to be done. This script helps automating offsite backups and thus more likely that they actually happen.

extshot is meant to be used as an BACKUP program invoked by cryptshot. It will copy directories with rotating names (as the ones created by rsnapshot) to a different location using stable names. Local backup directories can be easily copied this way for long-term storage to an external disk that may be moved offsite. This helps to implement the 3-2-1 backup strategy.

Setup

  1. Prepare the external disk like described in this article: https://pig-monkey.com/2012/09/cryptshot-automated-encrypted-backups-rsnapshot/.

  2. Install cryptshot and extshot:

    sudo wget https://raw.githubusercontent.com/pigmonkey/cryptshot/master/cryptshot.sh -O /usr/local/bin/cryptshot.sh
    sudo wget https://gist.githubusercontent.com/raw/c19f707e02a274f6371697163ee18b9f/extshot.sh -O /usr/local/bin/extshot.sh
    sudo chmod +x /usr/local/bin/cryptshot.sh /usr/local/bin/extshot.sh
    
  3. Create a configuration file for cryptshot (the UUID-style name is just a convention for distinguishing several external disks):

    # /etc/cryptshot-12929c96-06a6-465e-a5c1-7ebeb58b183d.conf
    export UUID=12929c96-06a6-465e-a5c1-7ebeb58b183d
    KEYFILE=/etc/12929c96-06a6-465e-a5c1-7ebeb58b183d.key
    BACKUP=/usr/local/bin/extshot.sh
    
  4. Find out the UUID of the disk (not the crypt partition) to derive the device unit name from it:

    # lsblk -o NAME,TYPE,MOUNTPOINT,UUID
    NAME                  TYPE  MOUNTPOINT UUID
    sdb                   disk             12929c96-06a6-465e-a5c1-7ebeb58b183d
    ...
    

    Now get the device unit name with systemd-escape -p /dev/disk/by-uuid/12929c96-06a6-465e-a5c1-7ebeb58b183d.

  5. Create a systemd service to trigger cryptshot automatically whenever the external disk is plugged in. Use the device unit name from the previous step for the Requires and WantedBy directives:

    # /etc/systemd/system/external-disk-12929c96-06a6-465e-a5c1-7ebeb58b183d.service
    [Unit]
    Description=Execute cryptshot when external disk is plugged in
    Requires=dev-disk-by\x2duuid-12929c96\x2d06a6\x2d465e\x2da5c1\x2d7ebeb58b183d.device
    
    [Install]
    WantedBy=dev-disk-by\x2duuid-12929c96\x2d06a6\x2d465e\x2da5c1\x2d7ebeb58b183d.device
    
    [Service]
    ExecStart=/usr/local/bin/cryptshot.sh -c /etc/cryptshot-12929c96-06a6-465e-a5c1-7ebeb58b183d.conf -i "gamma"
    

The optional -i will be passed to extshot and used as a filter to only copy the backup directories which contain this string.

Now run a systemctl daemon-reload and be delighted that your local rsnapshot backups are automatically copied to an encrypted disk as soon as you plug it in.

#!/usr/bin/env bash
BACKUP_FILTER=${1:-""}
RSNAPSHOT_CONFIG=/etc/rsnapshot.conf
function log {
logger -p local0.notice -t backup "$1"
}
log "$0 is starting..."
if [ ! -r $RSNAPSHOT_CONFIG ]
then
log "rsnapshot config file not readable."
exit 1
fi
RSNAPSHOT_ROOT=$(grep -i ^snapshot_root $RSNAPSHOT_CONFIG | cut -f2)
RSNAPSHOT_LOCKFILE=$(grep -i ^lockfile $RSNAPSHOT_CONFIG | cut -f2)
if [ -f "$RSNAPSHOT_LOCKFILE" ]
then
RSNAPSHOT_PID=$(cat "$RSNAPSHOT_LOCKFILE")
if ps --pid "$RSNAPSHOT_PID" > /dev/null
then
log "lockfile $RSNAPSHOT_LOCKFILE exists and is used by process $RSNAPSHOT_PID"
exit 1
else
log "stale lockfile $RSNAPSHOT_LOCKFILE found, proceeding."
fi
fi
echo $$ > "$RSNAPSHOT_LOCKFILE"
trap 'rm -f "$RSNAPSHOT_LOCKFILE"' EXIT
if [ "$UUID" = "" ]
then
log "No volume specified."
exit 1
fi
mountpoint=$(grep "${UUID}" /proc/mounts | cut -d' ' -f2)
if [[ -z "${mountpoint}" ]]
then
log "Volume with UUID ${UUID} is not mounted."
exit 1
fi
starttime=$(date)
find "${RSNAPSHOT_ROOT}" -mindepth 1 -maxdepth 1 -type d -printf "%T@ %p\n" \
| grep "${BACKUP_FILTER}" \
| sort \
| cut -d' ' -f2 \
| while read -r backup_dir
do
backup_dir_mtime=$(stat -c %Y "${backup_dir}")
backup_dir_time=$(date -d @"${backup_dir_mtime}" +%F)
target="${mountpoint}/${HOSTNAME}-${backup_dir_time}"
if [ -d "$target" ]
then
log "target $target already exists, skipping..."
continue
fi
/usr/bin/cp -a "${backup_dir}" "${target}"
done
endtime=$(date)
runtime=$(( $(date -d "$endtime" +%s) - $(date -d "$starttime" +%s) ))
log "$0 started at $starttime and ended at $endtime, total runtime: $runtime seconds"
# vim: ts=4 sw=4 expandtab
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment