en · November 17, 2025
Your Mail-in-a-Box Backup Strategy is Probably Backwards
TC

Your MIB backups are probably one server compromise away from being deleted. Here's why, and how to fix it.
Most people running Mail-in-a-Box back up the wrong way. They point the mail server at remote storage and let it push backups out. Feels logical: the server has the data, so the server sends it somewhere safe.
The worry is right. The usual fix isn't.
The real problem: who can delete your backups
Your mail server faces the internet, parses hostile input all day, and runs a big software stack. Assume it gets compromised eventually. Then ask one question:
Once the attacker owns that box, what can they reach?
If the mail server pushes, it holds credentials that can write and delete at the destination. So the attacker wipes your backups before you notice. A competent one does exactly that. Backup and target share a fate. That's the liability.
"Push bad, pull good" is too crude, though. The property you want is simple: your backups can't be deleted by whoever controls the mail server. There are two clean ways to get it. Use both.
Fix 1: You control the destination, so pull, and lock the key down
Back up to a NAS you own? Have the NAS pull, so the mail server never holds credentials to the backup store. Good.
But the naive version recreates the same problem in reverse. Hand the NAS a root@mailserver key and disable host-key checking, and you've made the NAS able to own the mail server, and the connection trivial to spoof. That's not a fix. It's a swap.
Do it right. The pulling key does exactly one thing — read one directory. Nothing else.
On the mail server, make an unprivileged user and give it read access to the duplicity output:
adduser --disabled-password --gecos "" backup-puller
setfacl -R -m u:backup-puller:rX /home/user-data/backup/encryptedRestrict the NAS's key to a read-only rsync of that path with rrsync (ships with rsync) and restrict, which kills port forwarding, agent forwarding, and PTY allocation. In /home/backup-puller/.ssh/authorized_keys:
command="rrsync -ro /home/user-data/backup/encrypted",restrict ssh-ed25519 AAAA... nas-backup-pullerNow a compromised NAS gets one thing: read access to an already-encrypted folder. No root, no shell, no lateral movement. This is the most important change from the naive setup.
Verify the host. Don't blindly trust it. Pin the mail server's key once:
ssh-keyscan -t ed25519 your-mail-server >> ~/.ssh/known_hosts_mailboxThe pull script (atomic, with retention)
The common script gets two things wrong. It isn't atomic, and it keeps no history.
It rm -rfs the destination and then copies the new data in. Crash mid-copy and you've destroyed your only backup. It also overwrites one folder every run, so a quiet compromise that corrupts the source gets pulled and wipes your last good copy within a day.
Fix both. Rename-based swap (rename is atomic on one filesystem) plus hardlinked dated snapshots via --link-dest for cheap deduplicated history:
#!/bin/bash
set -euo pipefail
MAIL_SERVER="backup-puller@<mail_in_a_box_ip>"
SSH_KEY="/home/nasuser/.ssh/id_ed25519_mailbox_puller"
KNOWN_HOSTS="/home/nasuser/.ssh/known_hosts_mailbox"
ROOT="/mnt/storage/mailserver-backups"
TODAY="${ROOT}/$(date +%F)"
STAGING="${ROOT}/.staging.$$"
LATEST="${ROOT}/latest"
mkdir -p "$STAGING"
# No -z. Duplicity output is already compressed and encrypted;
# compressing again just burns CPU. --link-dest dedupes against yesterday.
rsync -a --delete \
${LATEST:+--link-dest="$LATEST"} \
-e "ssh -i ${SSH_KEY} -o UserKnownHostsFile=${KNOWN_HOSTS} -o StrictHostKeyChecking=yes" \
"${MAIL_SERVER}:./" \
"${STAGING}/"
# Real check: the newest duplicity manifest must be recent.
# A non-empty folder is NOT a good backup.
newest=$(find "$STAGING" -name 'duplicity-full*.manifest*' -o -name 'duplicity-inc*.manifest*' \
| xargs -r stat --format '%Y' | sort -n | tail -1)
if [ -z "${newest:-}" ] || [ "$newest" -lt "$(date -d '2 days ago' +%s)" ]; then
echo "No recent duplicity manifest — backup is stale or failed" >&2
rm -rf "$STAGING"
exit 1
fi
# Atomic publish: rename staging into today's snapshot, then repoint 'latest'.
mv "$STAGING" "$TODAY"
ln -sfn "$TODAY" "$LATEST"
# Retention: keep 14 daily snapshots.
ls -1d "${ROOT}"/20* 2>/dev/null | sort | head -n -14 | xargs -r rm -rfEach day is its own snapshot and mv is atomic, so a failed run never touches a good backup, and yesterday survives even if today's source was poisoned. --link-dest means unchanged files cost no extra disk.
For real confidence, run duplicity verify or a test restore. That needs the passphrase, so run it on the mail server or a dedicated restore-test box. Never ship the passphrase to the NAS.
Fix 2: You still need an offsite, immutable copy, and it gets pushed
A NAS on your LAN is one copy in one building. Fire, theft, or ransomware moving across your network takes the mail server and the NAS together. Pull-to-NAS is two-thirds of 3-2-1 at best.
The backstop is offsite storage that can't be deleted even with valid credentials: object storage with Object Lock (S3 Object Lock in compliance mode, Backblaze B2, Wasabi with object lock). That copy gets pushed from the mail server, because there's no server on the object-storage side to pull.
Say it plainly: immutability beats direction of transfer. The reason to avoid push was "a compromised server can delete the destination." Object Lock removes that ability. The mail server's credentials can write new versions but physically cannot delete or overwrite locked objects until retention expires. Against ransomware and quiet deletion, immutable push storage is stronger than a mutable NAS you pull to. Not weaker.
Mail-in-a-Box already runs duplicity nightly into /home/user-data/backup/encrypted. Push it to a locked bucket with an after-backup hook at /home/user-data/backup/after-backup:
#!/bin/bash
aws s3 sync /home/user-data/backup/encrypted/ \
s3://your-bucket/mailserver/ --only-show-errorsConfigure the bucket for Object Lock and versioning. Give the mail server's key PutObject but not DeleteObject or retention-bypass. Set that in IAM. Let a lifecycle policy handle expiry, not the server.
Putting it together
Local, fast, cheap to restore: NAS pulls over a locked-down, read-only, non-root key. Dated snapshots with retention.
Offsite, deletion-proof: mail server pushes to immutable object storage it can't erase.
Confidence: periodic
duplicity verifyor test restore. Not "the folder isn't empty."
Two copies, two locations, one immutable. The direction of each transfer follows from one rule: nothing an attacker controls should be able to destroy your backups. Sometimes that means pulling. Sometimes it means pushing to storage that refuses to delete. Usually both.
Restores
The encrypted duplicity archives are already on the NAS, so restore is direct:
# copy the chosen snapshot back to the mail server, then:
export PASSPHRASE="your-encryption-passphrase"
duplicity restore file:///home/user-data/backup/encrypted /path/to/restoreIf the NAS copy is gone too, pull from the immutable bucket. That's the whole point of keeping two.
This reflects one working setup; yours will differ. The script is deliberately compact to make the shape clear. Production needs logging, alerting, and scheduled restore tests. The principle holds: design your backups so that whoever compromises a machine still can't delete what it backed up.