Send each verified backup to a destination plugin, encrypted with age (#325) #1071
No reviewers
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo!1071
Loading…
Reference in a new issue
No description provided.
Delete branch "325-backup-destinations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A copy of the instance on the same disk is no help the day the disk goes, and the core gave
a plugin no way to receive one: no kind was about backups, and the only instance-level
plugin settings were the transports'. This adds that kind and everything around it.
The kind:
backup-destination, entry-point grouppostulo.backups, withBackupDestinationPlugin(config_fields, test, send, list, delete; validate optional, asfor a connected plugin),
BackupArchive,BackupManifestandBackupRefonpostulo.plugins.api. It is ungoverned, like a transport, and wired into GROUPS, LABELS andTONES.
postulo.plugins.testingpublishesassert_is_a_backup_destination, which sends,lists and deletes one archive through a destination: what retention relies on.
Settings: a trio on SiteSettings like the transports' (plugin name, JSON configuration,
secrets under the field key), drawn from the plugin's FieldSpecs on a page of its own under
Server settings -> Backups, secrets masked and kept when left blank, a log line naming who
changed the destination. A destination is refused without an age recipient.
Encryption: the core encrypts each archive with pyrage (MIT, abi3 wheels for linux amd64 and
aarch64, maintained; 1.2.0-1.2.2 had GHSA-47h8-jmp3-9f28, so the floor is 1.4) to an X25519
recipient, the only kind accepted: a plugin recipient names a program for age to run. Only
the public key is stored. The encrypted copy is a scratch file deleted after the send.
Sending: an archive verified into POSTULO_BACKUP_DIR (schedule, Back up now, or
manage.py backupunless --no-send) queues an OffsiteBackup row. The scheduler sends itafter its pass's own work, in the worker when there is one, as document copies are sent:
claim with a lease, retry with a doubling wait, six attempts, then an administrator's Send.
The upload runs inside a deadline that ends before the lease. Remote retention follows only
a confirmed send, keeps the newest N (its own number, the local one when blank, 0 for none)
and deletes only references the plugin's list() returned and Postulo itself sent. Local
retention never rotates an archive still waiting for the destination.
What staff see: Sent/Waiting/Failed (in the plugin's words) per archive, a Send button, a
failed send on Overview, and postulo_backup_offsite_last_success_timestamp_seconds and
postulo_backup_offsite_failures_total. Every new address is staff only and a 404 otherwise;
each send and remote delete is logged. THREAT-MODEL gains the destination as an attacker.
Closes #325
Notes for review
SiteSettings.manage.py backupqueues a send, with--no-sendto skip.pyrage>=1.4,<2.list()returned and Postulo itself sent, so a destination's other files are never touched.core 0039_backup_destination.🤖 Generated with Claude Code
a63b47d31fabc968c2e8abc968c2e8a8fda0cd0ba8fda0cd0bffec2c933f