# 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-.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