AkurAI Build
Menu

AkurAI-Build

public

Latest 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