rsync Backup Security in Practice: Encrypted Backup and Restore Drill Configuration
Ransomware operators encrypt or delete backups first. Complete rsync backup security: SSH transport encryption, GPG at-rest encryption, authorized_keys single-command authorization, link-dest multi-version snapshots retained 14 days, plus full monthly restore drill and accidental-delete rollback. For offsite website and database backups.
Applicable Scenarios
The rsync backup security design fits: offsite cross-server backup of website data and databases; concern that backup traffic is sniffed or backup copies are encrypted for ransom; requirement that backups stay unreadable after backup-host compromise; mandatory periodic restore drills to prove availability. In ransomware incidents, operators typically encrypt or delete backups before extortion. Backup encryption and isolation matter as much as the backups themselves. This article gives a deployable rsync encrypted-backup and restore-drill path.
Prerequisites
- Two servers: production (source) and backup (offsite), both SSH-reachable
- Linux ships rsync (default on CentOS/Ubuntu); install gpg on the backup host for at-rest encryption
- Dedicated account on the backup host; write access limited to the backup directory
- Know daily incremental volume to size the cron window
How It Works
Three layers. Layer 1: transport encryption. rsync runs over ssh; the path is ciphertext. Avoids plaintext risk of FTP and bare rsync ports. Layer 2: at-rest encryption. Archives are gpg-encrypted before they land on the backup host. Keys are not stored on the backup host. Compromise yields ciphertext only. Layer 3: pull mode and privilege isolation. The backup host pulls. Its production account is read-only. Compromising production cannot delete backups. Incremental snapshots use link-dest: hard-link unchanged files, save space, keep independently restorable daily versions.
Procedure
Step 1: Passwordless trust, then tighten authorization
# Generate a dedicated key pair on the backup host
ssh-keygen -t ed25519 -f ~/.ssh/id_backup -N ""
# Upload the public key to production
ssh-copy-id -i ~/.ssh/id_backup.pub backup@PRODUCTION_IP
# Restrict authorized_keys on production: this key may only run rsync
# Prefix (one line):
command="rsync --server --sender -e . /backup-src/",no-pty,no-port-forwarding ssh-ed25519 AAAA... backup@backup-server
Step 2: Create the pull backup script
On the backup host, create `/opt/scripts/backup_pull.sh`:
#!/bin/bash
set -e
DATE=$(date +%F)
YESTERDAY=$(date -d "1 day ago" +%F)
DEST=/backup/web/$DATE
SRC=backup@PRODUCTION_IP:/data/www/
mkdir -p "$DEST"
# --link-dest points at yesterday's snapshot for hard-link incrementals; --delete mirrors deletions
rsync -az --delete \
--link-dest=/backup/web/$YESTERDAY \
-e "ssh -i ~/.ssh/id_backup -p 22" \
"$SRC" "$DEST/"
# After DB dump, gpg-encrypt to disk (public-key encrypt; private key stored offline)
rsync -az -e "ssh -i ~/.ssh/id_backup" \
backup@PRODUCTION_IP:/backup/db/dump-$DATE.sql.gz /tmp/
gpg --encrypt --recipient ops@company.com \
--output /backup/db/dump-$DATE.sql.gz.gpg /tmp/dump-$DATE.sql.gz
shred -u /tmp/dump-$DATE.sql.gz
# Keep 14 days; prune the rest
find /backup/web -maxdepth 1 -type d -mtime +14 -exec rm -rf {} \;
chmod 700 /opt/scripts/backup_pull.sh
# Add cron; run daily at 02:30
echo "30 2 * * * root /opt/scripts/backup_pull.sh >> /var/log/backup_pull.log 2>&1" > /etc/cron.d/backup-pull
Step 3: Configure backup integrity checks
Production generates a checksum when dumping the database. The backup host compares after each pull:
# Production: sha256 after dump
sha256sum dump-$(date +%F).sql.gz > dump-$(date +%F).sql.gz.sha256
# Backup host: verify after decrypt
gpg --decrypt dump-$DATE.sql.gz.gpg | sha256sum
diff <(sha256sum dump-$DATE.sql.gz) dump-$DATE.sql.gz.sha256 && echo OK
Step 4: Run the restore drill
Run a restore drill at least monthly. Prove the backup actually restores:
# 1. Restore the latest snapshot into an isolated directory (never overwrite production)
rsync -az /backup/web/$(date +%F)/ /restore-test/web/
# 2. Decrypt the DB backup and restore into a test database
gpg --decrypt dump-$DATE.sql.gz.gpg > /tmp/restore.sql.gz
gunzip -c /tmp/restore.sql.gz | mysql -h127.0.0.1 -utest -p test_db
# 3. Spot-check key files
diff -rq /restore-test/web/ /data/www/ | head
# 4. Log drill date, restore duration, and sample results into the ledger
Configuration Verification
# Confirm cron ran and the log is clean
tail -20 /var/log/backup_pull.log
# Confirm dated snapshot dirs exist and usage looks normal
ls -1 /backup/web/ | tail -5
du -sh /backup/web/*
# Confirm DB backups are gpg-encrypted
file /backup/db/dump-$(date +%F).sql.gz.gpg
# Confirm the private key is not on the backup host
find / -name "*secret*" -path "*gnupg*" 2>/dev/null
FAQ
FAQ 1: Will --delete sync accidental production deletes onto the backup?
Yes. --delete makes the backup side identical to the source. Source-side accidental deletes propagate. Two mitigations: dry-run first and review the change list; or keep multi-day link-dest snapshots (14 days here) and recover from a historical snapshot. That is the point of versioned snapshots.
FAQ 2: If production is compromised, can the attacker use the trust key to delete backups?
Risk is minimized under this config. Backups are pull-only from the backup host. The production authorized key is restricted to a read-only rsync command (authorized_keys command= prefix). The gpg private key never lands on the backup host. Do not put the backup host's write key on production in push mode. That lets a production compromise take the backups with it.
Summary
rsync + ssh + gpg delivers transport encryption, at-rest encryption, versioned snapshots, and restore verification at minimal cost. Deploy: pull instead of push; authorized_keys single-command grant; gpg public-key encrypt with private key offline; link-dest 14-day snapshots; monthly restore drills with a ledger. A backup only has value at the moment it restores. Do not skip drills.