Files
go-synapse-backupper/README.md
T

7.4 KiB

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 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:

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_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:

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", "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:

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