Surfaces
A surface is one thing worth protecting: a database, a file tree or a mailbox. Each Fire Drill tests one surface. All surfaces share one pipeline: stream, compress with zstandard, encrypt to your age public key and write to locked storage. They differ only in how the data is read at the start and checked at the end.
Plaintext never crosses a third-party network: the stream is encrypted on the host that read it. It never touches disk either, with one exception: a SQLite surface is copied to a private, owner-only temporary file on that same host first (SQLite), and the file is deleted as soon as it is uploaded.
The surfaces
| Surface | What is captured | The host needs |
|---|---|---|
| PostgreSQL | Schema from pg_dump, rows over binary COPY, one snapshot | pg_dump at least as new as the server |
| MySQL and MariaDB | mysqldump --single-transaction | The MySQL client |
| MongoDB | mongodump --archive | The MongoDB Database Tools |
| SQLite | One committed state, through VACUUM INTO | Nothing |
| Files | A directory tree, incremental: each run uploads only what changed | Read access to the tree |
| Every message of an IMAP mailbox, as RFC 5322 | The mailbox's password or app password |
Something you need that is not in the table (Microsoft 365 mail, MSSQL, Redis, ClickHouse, Elasticsearch, Windows hosts, Kubernetes volumes): request a surface.
What a Fire Drill proves, per surface
A Fire Drill always pulls the snapshot back from your bucket and decrypts it with your key. What it checks after that depends on the surface.
| Surface | What the drill checks |
|---|---|
| PostgreSQL | The table count and every table's row count match the
manifest, each table's binary COPY stream is readable, and the extensions are present.
By default this is done in memory, with no database running anywhere, which checks the
data but never runs the schema. Only a sandbox drill proves the schema loads: with
--sandbox-target the snapshot is restored into a scratch database you name
and counted there, and the daemon does the same for a surface with
drill.sandbox_url. On a plan with sandbox drills, a surface without one
restores into a throwaway cluster the daemon starts from the host's PostgreSQL server
(Daemon). |
| MySQL and MariaDB | The dump is complete, ending with its completion line. Every
table the manifest names is in it, with exactly the recorded number of rows. In memory, that
checks the data. A sandbox drill (--sandbox-target mysql://… or
drill.sandbox_url) loads the dump into an empty database, counts every table,
and empties the sandbox again. |
| MongoDB | Every collection and view in the archive reached its end.
Every collection's documents match the checksum mongodump wrote and the
count the manifest recorded. A sandbox drill restores into an empty database with
mongorestore, counts every collection, and drops them again. |
| SQLite | A real restore, every time: the database is written out, opened
by the SQLite engine, passes PRAGMA integrity_check, and every table's row
count matches the manifest. The file is deleted after. |
| Files | The whole snapshot restores into a scratch directory, every file
matching its SHA-256, and the digest recomputed from the restored files matches the one
recorded at backup time. A --format tar archive is checked in memory against
the manifest sealed inside it. |
| The message count matches the manifest, every message parses as RFC 5322 mail, and every message's SHA-256 matches. Nothing is written to disk. |
None of these replaces a rehearsed restore, which proves that a person can get the data back. A drill proves that each snapshot can be read back whole, every time, without anyone remembering to check.
Running several surfaces on one host
Every example above is a one-shot command. To protect more than one thing from a single host, describe the surfaces in the config file and let the daemon run them on a schedule:
Each entry registers as its own node and counts as one protected surface against
your plan quota. id must be stable: it is how state, locks and snapshot
history are keyed. A config with no surfaces: list still loads as a
single implicit Postgres surface, so no existing install needs an edit. The full
shape is in Daemon.
Adding a surface to a host that is already running
The host's config is the one list of what it protects, so a surface is added there, in one of two ways:
- From the console. Add surface on the host's row under
Nodes names the surface, its schedule and where its credential comes from.
Nothing changes on the host until you run
safegrd claimon it, which adds the surfaces named for that host to its config. It only adds: a surface the config already has is left as it is, the previous file is kept asconfig.yaml.bak, and running it again adds nothing. A credential SafeGrd holds is typed once in the console and never written to the file. - By hand. Add an entry under
surfaces:, as above.
Either way, restart the daemon afterwards, or run safegrd daemon run --once to take
the first backups now.