Synapse Backupper
PostgreSQL backup tool for Synapse with composite dual-KEM encryption.
Overview
synapse-backupper creates encrypted PostgreSQL dumps using a composite encryption scheme that combines a post-quantum KEM (ML-KEM-768) with a classical KEM (X25519). Both keys are required to decrypt a backup (AND model). The tool supports two operational modes: a one-shot backup command and a scheduler mode (run) that performs backups on a cron schedule.
Backups are stored locally on disk; retention pruning is applied automatically based on age.
Install
Build the Docker image:
make docker-build
This produces synapse-backupper:latest. The image contains the compiled binary, PostgreSQL client tools, and runs as an unprivileged user.
Quick Start
1. Generate encryption keys
Generate both key pairs (post-quantum and classical) with the keygen subcommand:
mkdir -p ./keys
docker run --rm -u root -v "$(pwd)/keys:/keys" synapse-backupper:latest \
keygen --type both --out-prefix /keys/synapse
chown -R "$(id -u):$(id -g)" ./keys
Note: The container image runs as an unprivileged
appuser by default.keygenmust write to the mounted host directory, so it is run as root here, then ownership is restored to the current user. Adjustchown(e.g., withsudo) if you are not running as root.
This creates four files under ./keys/:
| File | Purpose |
|---|---|
synapse.pq.pub.pem |
Post-quantum public key (for backup) |
synapse.pq.priv.pem |
Post-quantum private key (for restore, keep offline) |
synapse.classical.pub.pem |
Classical public key (for backup) |
synapse.classical.priv.pem |
Classical private key (for restore, keep offline) |
Important: Private keys should never be mounted into the backup container. Store them offline or on a separate restore-only host.
2. Run modes
Scheduler mode (run)
The default command starts a cron scheduler and an HTTP health-check endpoint:
docker run -d \
--name synapse-backupper \
-v "$(pwd)/keys:/keys:ro" \
-v "$(pwd)/backups:/backups" \
-e APP_PG_HOST=db \
-e APP_PG_DATABASE=synapse \
-e APP_PG_USER=synapse \
-e APP_PG_PASSWORD=secret \
-e APP_PQ_PUBLIC_KEY_PATH=/keys/synapse.pq.pub.pem \
-e APP_CLASSICAL_PUBLIC_KEY_PATH=/keys/synapse.classical.pub.pem \
-e APP_BACKUP_DIR=/backups \
synapse-backupper:latest
By default backups run at 03:00 daily. The schedule is controlled by backup.cron in the config file or APP_BACKUP_CRON.
One-shot backup (backup)
Run a single backup immediately:
docker run --rm \
-v "$(pwd)/keys:/keys:ro" \
-v "$(pwd)/backups:/backups" \
-e APP_PG_HOST=db \
-e APP_PG_DATABASE=synapse \
-e APP_PG_USER=synapse \
-e APP_PG_PASSWORD=secret \
-e APP_PQ_PUBLIC_KEY_PATH=/keys/synapse.pq.pub.pem \
-e APP_CLASSICAL_PUBLIC_KEY_PATH=/keys/synapse.classical.pub.pem \
-e APP_BACKUP_DIR=/backups \
synapse-backupper:latest backup
The resulting file is named synapse-<timestamp>.dump.pqenc and placed in the backup directory. The underlying dump is produced in PostgreSQL custom format (--format=custom), which is the format expected by pg_restore.
3. Restore a backup on a separate host
Restoration is intentionally performed on a host that does not have access to the database. Only the private keys are required.
docker run --rm \
-v "$(pwd)/keys:/keys:ro" \
-v "$(pwd)/backups:/backups:ro" \
-v "$(pwd)/restored:/out" \
synapse-backupper:latest restore \
--in /backups/synapse-20260102-150405.dump.pqenc \
--privkey-pq /keys/synapse.pq.priv.pem \
--privkey-classical /keys/synapse.classical.priv.pem \
--out /out/restored.dump
Then restore the plaintext dump with standard PostgreSQL tools:
pg_restore --clean --if-exists --dbname=synapse restored.dump
Configuration
The tool reads configuration in the following priority order: command-line flags → APP_* environment variables → config file → defaults.
Quick Start examples use environment variables. The prefix is APP_, and nested keys use underscores, for example:
APP_PG_HOSTAPP_PG_DATABASEAPP_BACKUP_DIRAPP_PQ_PUBLIC_KEY_PATHAPP_CLASSICAL_PUBLIC_KEY_PATH
To use a config file instead, generate an example and mount it into the container:
docker run --rm synapse-backupper:latest generate-config --lang en > config.yaml
Edit config.yaml, then mount it at /config.yaml when running the container:
docker run -d \
--name synapse-backupper \
-v "$(pwd)/config.yaml:/config.yaml" \
-v "$(pwd)/keys:/keys:ro" \
-v "$(pwd)/backups:/backups" \
synapse-backupper:latest
Environment variables override values from the config file.
Docker Compose
The scheduler can also be run with Docker Compose. Example docker-compose.yml:
version: '3.8'
services:
synapse-backupper:
image: synapse-backupper:latest
volumes:
- ./backups:/backups
- ./keys:/keys:ro
environment:
- APP_PG_HOST=synapse-pg
- APP_PG_DATABASE=synapse
- APP_PG_USER=synapse
- APP_PG_PASSWORD=secret
- APP_PQ_PUBLIC_KEY_PATH=/keys/synapse.pq.pub.pem
- APP_CLASSICAL_PUBLIC_KEY_PATH=/keys/synapse.classical.pub.pem
- APP_BACKUP_DIR=/backups
- APP_BACKUP_CRON=0 0 3 * * *
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 30s
timeout: 3s
start_period: 10s
restart: unless-stopped
Important: Only public keys (
*.pub.pem) should be present in./keys. Move private keys (*.priv.pem) to an offline or restore-only host before starting the container.
Start the scheduler with:
docker compose up -d
Security Model
Defence in depth
Backups are encrypted with a composite scheme: a shared secret is derived independently from both the post-quantum and classical KEMs, and the payload is encrypted with a symmetric key derived from both secrets. An attacker must break both KEMs to recover the data.
AND model
Decryption requires both private keys. The restore command will fail if either key is missing or incorrect. This prevents a single compromised key from exposing backups.
Plug-and-play KEM
KEM implementations are registered in a runtime registry by scheme ID. New post-quantum or classical algorithms can be added without changing the backup or restore logic.
Private key isolation
Private keys are not needed for backup creation. The backup container only mounts public keys (:ro). Private keys should live on a separate restore host or offline storage, reducing the blast radius of a backup-server compromise.
Limitations
This is an MVP release. The following features are not implemented:
- S3 / object storage — backups are written to a local directory only.
- Kubernetes deployment — no Helm chart or operator is provided.
- Streaming / incremental backups — each run produces a full
pg_dump. - Multi-database support — a single database per instance is assumed.
Development
Run unit tests:
make test
Run the end-to-end integration test (requires Docker):
make integration-test
Cross-compile:
make cross-build
License
MIT