feat: daily encrypted database backup to a bucket of its own
All checks were successful
Build and deploy / Validate source (push) Successful in 8s
Build and deploy / Integration suite on a real stack (push) Successful in 3m38s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images (push) Successful in 1m2s

A backup service runs pg_dump every day at 03:00 Brasília, checks the archive,
encrypts it with age to a public key and uploads it with a token for that
bucket only. The server cannot read or delete backups: the private key stays
with the owner, the bucket's lifecycle rule expires copies and its lock stops
early deletion. Each run is recorded and shown on the Kanban's Integrations
tab. tests/backup_test.py backs up, restores into a scratch database and
compares the rows in CI. Setup and restore: docs/BACKUP.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-29 18:49:21 -03:00
parent 1b57313496
commit 20403c5132
14 changed files with 565 additions and 2 deletions

View File

@@ -1,8 +1,11 @@
# Same pinned base as deploy/Dockerfile.api, so the integration suite exercises
# the image that ships rather than a different one.
FROM python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9
# pg_dump and age are for the database backup (ops/db_backup.py). Debian 13
# ships PostgreSQL 17, the server's major version, which pg_dump must match.
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends postgresql-client-17 age \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY infra/requirements.txt infra/requirements.lock /app/infra/

View File

@@ -21,4 +21,13 @@ printf '%s' "$policy" > /tmp/policy.json
mc admin policy create local dtf-artwork /tmp/policy.json >/dev/null
mc admin policy attach local dtf-artwork --user "$S3_APP_USER" >/dev/null
mc ilm import "local/$S3_BUCKET" < /lifecycle.json
# The backup bucket and its own account, which can write and read backups but
# not delete them: the same separation as the backup token on R2.
case "$S3_BACKUP_BUCKET" in *[!a-z0-9.-]*|'') echo 'Invalid local backup bucket name' >&2; exit 1;; esac
mc mb --ignore-existing "local/$S3_BACKUP_BUCKET" >/dev/null
mc anonymous set none "local/$S3_BACKUP_BUCKET" >/dev/null
mc admin user add local "$S3_BACKUP_USER" "$S3_BACKUP_PASSWORD" >/dev/null
printf '%s' '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:ListBucket","s3:GetBucketLocation"],"Resource":["arn:aws:s3:::'"$S3_BACKUP_BUCKET"'"]},{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject","s3:AbortMultipartUpload","s3:ListMultipartUploadParts"],"Resource":["arn:aws:s3:::'"$S3_BACKUP_BUCKET"'/*"]}]}' > /tmp/backup-policy.json
mc admin policy create local dtf-backup /tmp/backup-policy.json >/dev/null
mc admin policy attach local dtf-backup --user "$S3_BACKUP_USER" >/dev/null
echo 'Local storage runtime account provisioned.'