232 lines
7.4 KiB
Markdown
232 lines
7.4 KiB
Markdown
# Synapse Backupper
|
|
|
|
PostgreSQL backup tool for [Synapse](https://github.com/element-hq/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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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 `app` user by default. `keygen` must write to the mounted host directory, so it is run as root here, then ownership is restored to the current user. Adjust `chown` (e.g., with `sudo`) 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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_HOST`
|
|
- `APP_PG_DATABASE`
|
|
- `APP_BACKUP_DIR`
|
|
- `APP_PQ_PUBLIC_KEY_PATH`
|
|
- `APP_CLASSICAL_PUBLIC_KEY_PATH`
|
|
|
|
To use a config file instead, generate an example and mount it into the container:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```yaml
|
|
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", "synapse-backupper", "healthcheck"]
|
|
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.
|
|
>
|
|
> Replace `APP_PG_PASSWORD=secret` with a strong credential before deploying to production; the value is only an example.
|
|
|
|
Start the scheduler with:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
make test
|
|
```
|
|
|
|
Run the end-to-end integration test (requires Docker):
|
|
|
|
```bash
|
|
make integration-test
|
|
```
|
|
|
|
Cross-compile:
|
|
|
|
```bash
|
|
make cross-build
|
|
```
|
|
|
|
## License
|
|
|
|
MIT
|