# 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
