Menu
AkurAI-Build
publicLatest change eaf13029155ddc5d724b48d33056601ba58dcca1 - Harden the mail operations against live behaviour by Ólafur Búi Ólafsson
set -euo pipefail
key_file="$1"
fingerprint="$2"
retention="$3"
dest_host="$4"
dest_dir="$5"
[[ "$fingerprint" =~ ^[A-F0-9]{40}$ ]]
[[ "$retention" =~ ^[0-9]+$ ]] && [ "$retention" -ge 2 ] && [ "$retention" -le 365 ]
[[ "$dest_host" =~ ^[A-Za-z0-9._-]+@[A-Za-z0-9._:-]+$ ]]
[[ "$dest_dir" =~ ^/[A-Za-z0-9._/-]+$ ]]
sudo test -s "$key_file"
sudo install -d -o root -g root -m 0700 /etc/akurai-mail-backup/gnupg
sudo install -d -o root -g root -m 0700 /var/backups/akurai-mail
sudo install -d -o root -g root -m 0755 /usr/local/libexec
# The offsite recipient key pair is generated on Titan: only the public half ever
# reaches this box, so a compromise here cannot read the archives it produced.
sudo gpg --batch --homedir /etc/akurai-mail-backup/gnupg --import "$key_file"
printf '%s:6:\n' "$fingerprint" |
sudo gpg --batch --homedir /etc/akurai-mail-backup/gnupg --import-ownertrust
sudo gpg --batch --homedir /etc/akurai-mail-backup/gnupg --with-colons --list-keys "$fingerprint" |
grep -q '^pub:'
if sudo gpg --batch --homedir /etc/akurai-mail-backup/gnupg --with-colons --list-secret-keys 2>/dev/null |
grep -q '^sec:'; then
echo 'a secret key must never be installed on the mail server' >&2
exit 1
fi
printf '%s\n' "$fingerprint" | sudo tee /etc/akurai-mail-backup/recipient.fingerprint >/dev/null
sudo chmod 0600 /etc/akurai-mail-backup/recipient.fingerprint
rm -f -- "$key_file"
sudo tee /usr/local/libexec/akurai-mail-backup >/dev/null <<'SCRIPT'
#!/usr/bin/env bash
# Encrypted mail-server snapshot pushed to Titan. Installed by `akurai-ec2 mail-backup install`.
set -euo pipefail
umask 0077
retention="${AKURAI_MAIL_BACKUP_RETENTION:-14}"
dest_host="${AKURAI_MAIL_BACKUP_DEST_HOST:?destination host is required}"
dest_dir="${AKURAI_MAIL_BACKUP_DEST_DIR:?destination directory is required}"
ssh_key="${AKURAI_MAIL_BACKUP_SSH_KEY:?mesh key is required}"
[[ "$retention" =~ ^[0-9]+$ ]] && [ "$retention" -ge 2 ] && [ "$retention" -le 365 ]
root=/var/backups/akurai-mail
state="$root/state"
fingerprint="$(</etc/akurai-mail-backup/recipient.fingerprint)"
[[ "$fingerprint" =~ ^[A-F0-9]{40}$ ]]
exec 9>/run/akurai-mail-backup/lock
flock -n 9 || { echo 'a mail backup is already running' >&2; exit 1; }
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
rm -rf -- "$state"
install -d -m 0700 "$state"
postconf -n >"$state/postconf.txt"
doveconf -n >"$state/doveconf.txt"
dpkg-query -W -f='${binary:Package}\t${Version}\n' >"$state/packages.tsv"
systemctl is-active postfix@- dovecot opendkim opendmarc spamass-milter \
>"$state/services.txt" || true
accounts="$(grep -c ':' /etc/dovecot/users 2>/dev/null || echo 0)"
messages="$(find /var/vmail -type f \( -path '*/cur/*' -o -path '*/new/*' \) | wc -l)"
bytes="$(du -sb /var/vmail | cut -f1)"
printf '{"stamp":"%s","host":"%s","accounts":%s,"messages":%s,"vmail_bytes":%s,"retention":%s}\n' \
"$stamp" "$(hostname -f)" "$accounts" "$messages" "$bytes" "$retention" >"$state/manifest.json"
[ "$messages" -gt 0 ]
paths=(var/vmail)
for path in etc/postfix etc/dovecot etc/opendkim etc/opendmarc etc/spamassassin \
etc/aliases etc/mailname; do
[ -e "/$path" ] && paths+=("$path")
done
name="akurai-mail-$stamp.tar.gz.gpg"
incoming="$root/.incoming-$stamp.tar.gz.gpg"
trap 'rm -f -- "$incoming"' EXIT
# Maildir files are immutable once delivered, so streaming them straight into the
# archive is consistent; tar's 1 means a file changed or vanished mid-read.
set +e
tar --numeric-owner --warning=no-file-changed --warning=no-file-removed \
-czf - -C / "${paths[@]}" -C "$root" state |
gpg --batch --yes --trust-model always --homedir /etc/akurai-mail-backup/gnupg \
--recipient "${fingerprint}!" --output "$incoming" --encrypt
status=("${PIPESTATUS[@]}")
set -e
[ "${status[1]}" = 0 ]
[ "${status[0]}" = 0 ] || [ "${status[0]}" = 1 ]
[ -s "$incoming" ]
[ "$(od -An -N2 -tx1 "$incoming" | tr -d ' \n')" != 1f8b ]
chmod 0600 "$incoming"
mv -f -- "$incoming" "$root/$name"
trap - EXIT
(cd "$root" && sha256sum "$name" >"$name.sha256")
chmod 0600 "$root/$name.sha256"
ssh_opts=(-o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=15 -i "$ssh_key")
rsync -a --partial-dir=.partial -e "ssh ${ssh_opts[*]}" -- \
"$root/$name" "$root/$name.sha256" "$dest_host:$dest_dir/"
ssh "${ssh_opts[@]}" "$dest_host" "RETENTION='$retention' DIRECTORY='$dest_dir' bash -s" <<'PRUNE'
set -euo pipefail
cd "$DIRECTORY"
ls -1t akurai-mail-*.tar.gz.gpg 2>/dev/null | tail -n "+$((RETENTION + 1))" |
while IFS= read -r old; do rm -f -- "$old" "$old.sha256"; done
PRUNE
# The box keeps only a short local window: Titan holds the retained history.
find "$root" -maxdepth 1 -type f -name 'akurai-mail-*.tar.gz.gpg' -printf '%T@ %p\n' |
sort -nr | awk 'NR > 3 { print $2 }' |
while IFS= read -r old; do rm -f -- "$old" "$old.sha256"; done
printf 'backup complete: %s messages=%s bytes=%s offsite=%s\n' \
"$name" "$messages" "$bytes" "$dest_host:$dest_dir"
SCRIPT
sudo chown root:root /usr/local/libexec/akurai-mail-backup
sudo chmod 0755 /usr/local/libexec/akurai-mail-backup
sudo tee /etc/systemd/system/akurai-mail-backup.service >/dev/null <<UNIT
[Unit]
Description=Encrypted mail-server snapshot pushed to Titan
After=network-online.target dovecot.service
Wants=network-online.target
[Service]
Type=oneshot
Environment=AKURAI_MAIL_BACKUP_RETENTION=$retention
Environment=AKURAI_MAIL_BACKUP_DEST_HOST=$dest_host
Environment=AKURAI_MAIL_BACKUP_DEST_DIR=$dest_dir
Environment=AKURAI_MAIL_BACKUP_SSH_KEY=/home/ubuntu/.ssh/id_ed25519_akurai_mesh
ExecStart=/usr/local/libexec/akurai-mail-backup
TimeoutStartSec=30min
RuntimeDirectory=akurai-mail-backup
RuntimeDirectoryMode=0700
UMask=0077
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadOnlyPaths=/var/vmail /etc/postfix /etc/dovecot /etc/opendkim /etc/opendmarc
ReadWritePaths=/var/backups/akurai-mail /etc/akurai-mail-backup/gnupg
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=default.target
UNIT
sudo tee /etc/systemd/system/akurai-mail-backup.timer >/dev/null <<'UNIT'
[Unit]
Description=Daily encrypted mail-server backup to Titan
[Timer]
OnCalendar=*-*-* 02:10:00 UTC
RandomizedDelaySec=10m
Persistent=true
Unit=akurai-mail-backup.service
[Install]
WantedBy=timers.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now akurai-mail-backup.timer
sudo systemctl start akurai-mail-backup.service