Menu
AkurAI-Build
publicLatest change b2fb6810b8844f39a9752f65118c05ffecf28160 - Fold AkurAI EC2 operations into Build CLI by Ólafur Búi Ólafsson
set -euo pipefail
retention="$1"
recipient="akurai-tasks-backup@olibuijr.com"
sudo install -d -o root -g root -m 0700 /etc/akurai-tasks-backup/gnupg
sudo install -d -o root -g root -m 0711 /var/backups/akurai-tasks
sudo install -d -o ubuntu -g root -m 2770 /var/backups/akurai-tasks/staging
sudo find /var/backups/akurai-tasks/staging -maxdepth 1 -type f \
\( -name 'akurai-tasks-*.db' -o -name 'akurai-tasks-*.db.manifest.json' \) -delete
sudo install -d -o root -g root -m 0755 /usr/local/libexec
if ! sudo gpg --batch --homedir /etc/akurai-tasks-backup/gnupg --with-colons --list-keys "$recipient" 2>/dev/null | grep -q '^pub:'; then
sudo gpg --batch --homedir /etc/akurai-tasks-backup/gnupg --passphrase '' \
--quick-generate-key "$recipient" rsa3072 encrypt 0
fi
fingerprint="$(sudo gpg --batch --homedir /etc/akurai-tasks-backup/gnupg --with-colons --fingerprint "$recipient" | awk -F: '$1 == "fpr" { print $10; exit }')"
[[ "$fingerprint" =~ ^[A-F0-9]{40}$ ]]
printf '%s\n' "$fingerprint" | sudo tee /etc/akurai-tasks-backup/recipient.fingerprint >/dev/null
sudo chmod 0600 /etc/akurai-tasks-backup/recipient.fingerprint
while IFS= read -r archive; do
listing="$(mktemp)"
if sudo gpg --batch --homedir /etc/akurai-tasks-backup/gnupg --decrypt "$archive" 2>/dev/null |
sudo tar -tf - >"$listing" 2>/dev/null &&
[ "$(grep -Ec '^akurai-tasks-[0-9]+-[0-9]+\.db$' "$listing")" -eq 1 ] &&
[ "$(grep -Ec '^akurai-tasks-[0-9]+-[0-9]+\.db\.manifest\.json$' "$listing")" -eq 1 ]; then
:
else
sudo rm -f -- "$archive"
fi
rm -f "$listing"
done < <(sudo find /var/backups/akurai-tasks -maxdepth 1 -type f -name 'akurai-tasks-*.tar.gpg' -print)
sudo tee /usr/local/libexec/akurai-tasks-backup >/dev/null <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
umask 0077
retention="${AKURAI_TASKS_BACKUP_RETENTION:-48}"
[[ "$retention" =~ ^[0-9]+$ ]] && [ "$retention" -ge 2 ] && [ "$retention" -le 720 ]
set -a
source /etc/akurai-tasks/akurai-tasks.env
set +a
: "${AKURAI_TASKS_MCP_TOKEN:?AKURAI_TASKS_MCP_TOKEN is required}"
directory=/var/backups/akurai-tasks
staging="$directory/staging"
config="$(mktemp)"
response="$(mktemp)"
backup=""
incoming=""
cleanup() {
rm -f "$config" "$response"
case "$backup" in
"$staging"/akurai-tasks-*.db) rm -f -- "$backup" "$backup.manifest.json" ;;
esac
case "$incoming" in
"$directory"/.incoming-*.tar.gpg) rm -f -- "$incoming" ;;
esac
}
trap cleanup EXIT
printf 'header = "Authorization: Bearer %s"\n' "$AKURAI_TASKS_MCP_TOKEN" >"$config"
curl --fail --silent --show-error --config "$config" \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"system_backup","arguments":{}}}' \
http://127.0.0.1:8108/mcp >"$response"
backup="$(python3 - "$response" <<'PY'
import json
import sys
document = json.load(open(sys.argv[1], encoding="utf-8"))
if "error" in document:
raise SystemExit(document["error"].get("message", "backup RPC failed"))
result = json.loads(document["result"]["content"][0]["text"])
print(result["backup"])
PY
)"
case "$backup" in "$staging"/akurai-tasks-*.db) ;; *) echo "unexpected backup path" >&2; exit 1 ;; esac
[ -s "$backup" ] && [ -s "$backup.manifest.json" ]
name="$(basename "$backup")"
stamp="$(date -u +%Y%m%dT%H%M%S)-$(date +%N)"
destination="$directory/akurai-tasks-$stamp.tar.gpg"
incoming="$directory/.incoming-$stamp.tar.gpg"
fingerprint="$(</etc/akurai-tasks-backup/recipient.fingerprint)"
tar -C "$staging" -cf - "$name" "$name.manifest.json" |
gpg --batch --yes --trust-model always --homedir /etc/akurai-tasks-backup/gnupg \
--recipient "${fingerprint}!" --output "$incoming" --encrypt
[ -s "$incoming" ]
mv -f -- "$incoming" "$destination"
incoming=""
rm -f -- "$backup" "$backup.manifest.json"
backup=""
mapfile -t backups < <(find "$directory" -maxdepth 1 -type f -name 'akurai-tasks-*.tar.gpg' -printf '%f\n' | LC_ALL=C sort)
excess=$((${#backups[@]} - retention))
if [ "$excess" -gt 0 ]; then
for ((index = 0; index < excess; index++)); do
rm -- "$directory/${backups[$index]}"
done
fi
SCRIPT
sudo chown root:root /usr/local/libexec/akurai-tasks-backup
sudo chmod 0755 /usr/local/libexec/akurai-tasks-backup
sudo tee /etc/systemd/system/akurai-tasks-backup.service >/dev/null <<UNIT
[Unit]
Description=Create a verified encrypted AkurAI-Tasks database backup
After=akurai-tasks.service
Requires=akurai-tasks.service
[Service]
Type=oneshot
User=root
Group=root
Environment=AKURAI_TASKS_BACKUP_RETENTION=$retention
ExecStart=/usr/local/libexec/akurai-tasks-backup
UMask=0077
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/opt/akurai-tasks /etc/akurai-tasks
ReadWritePaths=/var/backups/akurai-tasks /etc/akurai-tasks-backup/gnupg
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
UNIT
sudo tee /etc/systemd/system/akurai-tasks-backup.timer >/dev/null <<'UNIT'
[Unit]
Description=Hourly encrypted AkurAI-Tasks database backup
[Timer]
OnCalendar=hourly
RandomizedDelaySec=5m
Persistent=true
Unit=akurai-tasks-backup.service
[Install]
WantedBy=timers.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now akurai-tasks-backup.timer
sudo systemctl start akurai-tasks-backup.service