Encrypted Offsite Backup With Ransomware Protection for WordPress
This article explains how to set up restic (with the resticprofile wrapper) for automated scheduled backups of your (dockerized) WordPress server. The backups are protected from ransomware through temporary immutability, which makes it much harder for attackers to delete your data.
Prerequisites
This post uses the server setup described in great detail in my Guide: WordPress on Dockerized Apache on Hetzner Cloud.
The backup configuration is based on my detailed article about restic: Encrypted Offsite Backup With Ransomware Protection for Your Homeserver.
All that’s left to do here is bring the two together.
Backup Strategy
We’re using restic (with the resticprofile wrapper) on the Docker host to back up both the host configuration as well as the Docker container data.
Backing Up the Database From a Running Docker Container
The simplest way to back up a running docker container is to stop it and copy all the files. I employed that strategy in my article about restic on my home server. In that scenario, it’s OK to stop Docker for the duration of the backup. It saves us the trouble of figuring out how to perform a hot backup for each container individually.
Obviously, things are different with a webserver. You want it to run 24/7. But there’s only one database involved and we know exactly how to perform a hot backup: we dump its contents into a .sql
file.
Ransomware Protection Through Temporary Immutability
If you back up WordPress by way of a plugin, like I did for a long time, the application keys to read from and, critically, write to the storage location need to be available to the plugin. If an attacker manages to compromise your WordPress instance they’ll have access to the backup storage – and they can wipe it or do other creative things with your backup data.
By moving the storage keys from the WordPress instance to the Docker host we make it a lot harder for attackers to gain access to them. They’d have to break out of the WordPress Docker container first. But that is not the end.
Due to the way restic is architected, we can even create an application key without delete capabilities. Thus, even if an attacker managed to get hold of all the data on your WordPress Docker host, they wouldn’t be able to wipe your backup data. See my restic article for more details.
Benefits Compared to WordPress Backup Plugins
The solution presented here has numerous benefits compared to a WordPress backup plugin:
- Better security
- Faster backups
- Higher performance
- Less backup storage
Backup Implementation
Follow the steps in my restic article to set up the (Backblaze or S3) storage and the restic/resticprofile tool combo.
As changes from the original setup, I placed the resticprofile configuration in /data/resticprofile/profiles.yaml
and modified the configuration slightly. Note that I’m also running a command to dump the WordPress database before the backup.
The following lists the full configuration:
default:
lock: "/tmp/resticprofile-profile-default.lock"
force-inactive-lock: true
initialize: true
repository: "s3:BUCKET_ENDPOINT_URL/YOUR_BUCKET_NAME" # Example: s3:s3.eu-central-003.backblazeb2.com/YOUR_BUCKET_NAME
password-file: "YOUR_BUCKET_NAME.key"
status-file: "backup-status.json"
env:
AWS_ACCESS_KEY_ID: "YOUR_KEY_ID"
AWS_SECRET_ACCESS_KEY: "YOUR_KEY"
# Backup command
backup:
run-before:
- 'docker exec mariadb /usr/bin/mariadb-dump --user=root --password=YOUR_DB_PASSWORD --lock-tables --all-databases > /data/docker/lamp/data/mariadb-backup/backup.sql'
run-finally:
- 'rm /data/docker/lamp/data/mariadb-backup/backup.sql'
one-file-system: true # Don't leave the file system via mount points
source:
- "/data/resticprofile"
- "/etc/ssh/sshd_config"
- "/etc/docker/daemon.json"
- "/etc/sysctl.conf"
- "/data/docker/caddy/Caddyfile"
- "/data/docker/caddy/caddy_security.conf"
- "/data/docker/caddy/container-vars.env"
- "/data/docker/caddy/docker-compose.yml"
- "/data/docker/lamp/config/apache-misc"
- "/data/docker/lamp/config/php"
- "/data/docker/lamp/config/vhosts"
- "/data/docker/lamp/data/mariadb-backup"
- "/data/docker/lamp/data/www"
- "/data/docker/lamp/dockerfile-apache"
- "/data/docker/lamp/container-vars-mariadb.env"
- "/data/docker/lamp/docker-compose.yml"
- "/etc/postfix/main.cf"
- "/etc/postfix/mynetworks"
- "/etc/postfix/sasl_passwd"
- "/etc/postfix/generic"
- "/etc/cron.weekly/00logwatch"
- "/etc/logwatch"
schedule: "04:00"
schedule-permission: system
schedule-lock-wait: 10m
schedule-log: '/data/resticprofile/backup.log'
verbose: 2 # Write details about each processed file to the log
# Retention policy command
forget:
keep-daily: 7
keep-weekly: 8
keep-monthly: 12
keep-yearly: 10
prune: true
schedule: "05:00"
schedule-permission: system
schedule-lock-wait: 1h
# Verify command
check:
schedule: "06:00"
schedule-permission: system
schedule-lock-wait: 1h
Before putting the above to use, create the directory /data/docker/lamp/data/mariadb-backup
and replace the following variables with values suitable for your environment:
- BUCKET_ENDPOINT_URL
- YOUR_BUCKET_NAME
- YOUR_KEY_ID
- YOUR_KEY
- YOUR_DB_PASSWORD