Files

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