BackupProof

How it works

A backup only counts once it has been restored and checked. Here’s exactly how BackupProof does that, and how anyone can verify the result without trusting it.

backup ──► snapshot ─► content root R  (BLAKE3 Merkle tree of every file)
              │
              ├─► signed statement: backup, root R ───────────────┐
              │                                                    │  signed by the
restore test ─► restore from storage ─► recompute R' from disk     │  server that did it
              ├─► R' = R ?  integrity checks · row counts · yours  │
              └─► signed statement: test result, root R, checks ──┤
                                                                   ▼
                    ledger: each entry hashes the one before it
                    signed checkpoint · optional RFC 3161 timestamp

Backing up

Files are split into variable-size chunks (FastCDC, about 1 MB on average), so a change in a big file only stores the changed parts, and identical data is stored once across files and backups. Each chunk is compressed with zstd and encrypted with XChaCha20-Poly1305 before it leaves the server.

Databases are copied with their own consistent dump tools (pg_dump, mysqldump --single-transaction, mongodump, SQLite’s VACUUM INTO), streamed straight into the backup. At the same time BackupProof records each table’s row count, so a restore test can later check that every row came back.

Every backup gets a content root: a BLAKE3 Merkle tree over each file’s path, size and content hash. Unlike chunk names, it uses no secret key, so anyone holding the restored files can recompute it.

Restore tests

On a schedule, and straight after the first backup, a server restores the newest backup from storage into an empty folder. It downloads and decrypts every chunk, checks every file against its hash, then recomputes the content root from the bytes on disk. If it doesn’t match the root recorded at backup time, the test fails.

Databases are then loaded into a temporary container with no network access, limited memory and CPU, and the image pinned by digest. You can run restore tests on a different server from the one that made the backup, so the test only uses what’s in storage.

Checks for each kind of data

DataWhat the restore test checks
All backupsEvery chunk decrypts and authenticates; every file matches its hash; the content root matches.
PostgreSQLThe dump loads with pg_restore; amcheck verifies every B-tree index against its table; row counts match the backup.
MySQL / MariaDBThe dump imports cleanly; CHECK TABLE passes; row counts match the backup.
MongoDBThe archive restores; every collection passes validate with full checks.
SQLitePRAGMA integrity_check passes; every table has exactly the rows it had.
Dumps inside file backupsPostgreSQL dumps found among your files are loaded into a test database too.
Your own checksSQL that must return true (“orders exist this week”), files that must be present, a minimum file count, or a command you write.

Signed proof

Each backup and each restore test, pass or fail, becomes an in-toto statement wrapped in a DSSE envelope and signed with the Ed25519 key of the server that did the work. The statement names the backup by its content root and lists every check with its result and the time to restore.

The dashboard checks each signature against the server’s enrolled key before accepting it, and only accepts statements about that server’s own jobs. Restore tests only ever restore the backup named in a proof the dashboard has already verified.

The ledger

Every proof, and every change an operator makes, is appended to a ledger where each entry includes the hash of the one before. Removing, editing or reordering an entry breaks the chain from that point on, and the database refuses updates and deletes outright. The dashboard signs checkpoints of the ledger head, and each proof can also carry an RFC 3161 timestamp from an independent authority, so dates can’t be backdated.

Verifying a proof

Download a proof bundle or a proof report and check it with the same open-source program, offline, without the storage password:

$ backupproof proof verify drill.bundle.json --key bpkey1:server@backup:… --require-timestamp
VALID  restore-drill/v1
  signer    agent:web-01 (bp:e96a62db83be1b16)
  drill     passed=true
  ledger    entry #13, chained to signed checkpoint #14
  time      2026-10-06T22:20:28Z (RFC 3161)

Get the public keys from the dashboard’s public keys page, ideally through a separate channel from the bundle itself.

Storage format

ObjectContents
configFormat version, repository ID and chunker settings. No secrets.
keys/Password slots. Argon2id turns a password into a key that unwraps the random master keys.
data/Encrypted chunks, named by a keyed BLAKE3 hash so storage can’t tell what’s inside. Every read re-checks the name.
snapshots/Encrypted backup records: source, time, content root, stats and file list.
proofs/Signed statements. Public by design: hashes and counts, never file names or data.

Keeping old copies

You choose how many daily, weekly, monthly and yearly copies to keep, with the same rules restic uses. One rule is always added: the newest copies that passed a restore test are never deleted. Space is reclaimed only from data no copy uses any more, and never while a backup might still be writing. Storage with S3 Object Lock can’t be deleted before its lock ends, which protects against ransomware.