Encrypted backups
The SDK can back up your database or any file/folder to Quiet Guard's zero-knowledge vault. Everything is encrypted on your server before upload, with a key only your team can unlock. The server stores an opaque blob and never sees your data or your key.
This page describes the latest version of the package. Commands and options added after v0.1 carry the version that introduced them; if one answers "command not found" or "option does not exist", your installation is older. Check yours withcomposer show quiet-guard/laravel-monitor, and update withcomposer update quiet-guard/laravel-monitor.
Prerequisites
- Encryption must be enabled for your team in the Quiet Guard dashboard (this generates the team key pair and is protected by a team passphrase). The backup vault is a paid feature subject to a storage quota. See the server documentation.
- PHP built with libsodium (
ext-sodium, bundled by default since PHP 7.2). - For non-SQLite databases, the matching dump binary on the host:
mysqldump(MySQL / MariaDB) orpg_dump(PostgreSQL). SQLite databases are copied directly.
Creating a backup
Options
| Option | Description |
|---|---|
--database | Back up the default database connection (config('database.default')). |
--path= | Back up a single file or a whole directory. |
--name= | Optional label stored alongside the backup. Printable ASCII, no slashes, up to 255 characters: the label becomes the filename the download is offered under, and a path separator is not something an HTTP header can carry. The server answers 422 and names the field when it refuses one. |
--keep= | Since v0.2.1. After a successful upload, keep the N newest backups of the same type and delete the rest. At least 1. |
--exclude-table= | Since v0.5.0. With --database, keep this table's structure but leave its rows out of the backup. Repeatable. Letters, digits, _, $ and - only: a qualified name (db.table) or a wildcard is refused, and the command stops rather than back the table up anyway. |
You must pass either --database or --path, and that is checked before the network call, so a command with no target is told so instead of being answered with an unrelated complaint about encryption. When MONITOR_ENABLED=false, the command does nothing.
When a step is refused, the command prints the server's own sentence rather than a guess: a 422 naming the field it refused, a 413 naming the storage quota that is full, a 403 on a plan without backups. Those four rejections used to arrive as the same advice, which fixed three of them not at all.
How the archive is built
--database: SQLite connections are copied as-is; MySQL/MariaDB are dumped withmysqldump, PostgreSQL withpg_dump. The password is passed through the environment (MYSQL_PWD/PGPASSWORD), never on the command line. The dump runs with a 600-second timeout. The backup is recorded with typedatabase.--path: a directory is packed into a tar archive (a single file is added to a tar), recorded with typefiles.
Excluding tables
Since v0.5.0. Some tables should not survive in a backup: daily salts a privacy policy promises to destroy within hours, sessions, a cache. A nightly backup would keep them for the whole retention of the vault.
The table's structure is kept and only its rows are left out: a restored database missing a table breaks the application that restores it, while an empty table is exactly what was asked for.
- MySQL / MariaDB: two
mysqldumppasses, each with--single-transaction. The first writes the structure of every table (--no-data), the second the rows of every table but the excluded ones (--no-create-info --skip-triggers --ignore-table=…). - PostgreSQL: one
pg_dumppass with--exclude-table-data=…. - SQLite: the copy is emptied of the excluded tables, then compacted (
VACUUM), or the deleted rows would stay readable in the file's free pages. The original database is not touched.
A table that does not exist is passed over, as mysqldump passes over an --ignore-table naming nothing: there are no rows to leave out.
Rotating old backups
Since v0.2.1.
Nothing on the server deletes a backup for you: the retention sweeps cover monitoring data, not the vault. A nightly schedule therefore fills the plan's storage allowance and then fails every night, from the scheduler, where nobody is reading. --keep=N closes that:
Three properties are worth knowing:
- rotation runs only after a successful upload, so the night the dump fails never trades a full vault for an empty one;
- it only ever deletes backups of the same type, so a nightly
--databaserotation leaves the folder backup you took by hand alone; - a delete that fails is reported but does not fail the command: the new backup is uploaded and safe, the vault is merely fuller than you asked, and telling a scheduler otherwise would say the backup did not happen.
Listing what the vault holds
Since v0.2.1.
monitor:restore takes a backup id as its only required argument, and the dashboard used to be the only place that id could be read. monitor:backups prints it from the shell:
One row per backup, newest first, with id, type, name, size and created, followed by the monitor:restore line to copy. An empty vault says so and points at monitor:backup --database. If the call is refused, what the server said is shown, so a wrong key, a wrong address and a plan without backups no longer read alike.
Restoring a backup
Options
| Argument / option | Description |
|---|---|
id (argument) | The backup id: printed by monitor:backups, returned by monitor:backup after the upload, and shown in the Identifier column of the dashboard's Backups tab. |
--output= | Where to write the decrypted result. Defaults to restored-<id>.out in the app base path. |
--passphrase= | The team passphrase. Prompted securely (hidden input) if omitted. |
--file= | Not released yet. Decrypts a blob you have already downloaded, instead of fetching it. The id becomes optional; your file is never deleted. |
The dashboard's Download button hands you the .enc file, which is the sealed blob: the server holds nothing else, and nothing opens it by hand. --file= is what decrypts it. The key material is still fetched from the server, because the private key only exists wrapped by your passphrase and is never stored locally; with no id the checksum is compared to nothing, and the command says so rather than letting you assume otherwise.
Before anything is decrypted, the client compares the downloaded blob against the sha256 the server publishes for it. A mismatch stops there, with a message of its own, and nothing is written to --output. A damaged download used to fail as "Decryption failed", which is exactly what a wrong passphrase produces, and those two are not repaired the same way. When the server publishes no checksum for that backup, or the list cannot be reached, the restore goes ahead: this check guards against a damaged transfer, it is not an authorisation.
The decrypted output is the raw archive: a SQL dump (or SQLite file) for --database backups, or a tar archive for --path backups. Because the default output is restored-<id>.out, pass --output with an appropriate extension when you want to use it directly, e.g. --output=restore.sql or --output=files.tar.
How the encryption works
Backups use hybrid streaming encryption, shared with every other Quiet Guard client through the quiet-guard/monitor-php core:
- A random symmetric key encrypts the archive with libsodium's secretstream (chunked XChaCha20-Poly1305 AEAD, 64 KiB chunks), so large dumps are streamed, never held whole in memory.
- That symmetric key is sealed to your team's X25519 public key (
crypto_box_seal). The sealed key is prepended to the blob. - The encrypted blob is uploaded to
/api/v1/backups. The server stores it opaquely, alongside metadata (type, name, size), and enforces your storage quota.
To restore, the SDK fetches your key material from /api/v1/encryption-key (public key, the passphrase-wrapped private key, and the KDF salt), then unwraps the private key locally from your passphrase: an Argon2id key-derivation (MODERATE limits) reproduces the key-encryption key and opens a libsodium secretbox, exactly mirroring the server's own wrapping. The unwrapped key opens the sealed symmetric key, which decrypts the stream.
This is zero-knowledge: the passphrase and the unwrapped private key never leave your server, and the operator is never able to read your backups. The trade-off is that a lost passphrase means the backup cannot be recovered, there is no reset.
Scheduling
Schedule nightly database backups from your application's scheduler:
Pass --keep on a scheduled backup unless you delete old ones some other way. Without it the vault only ever grows, and the first thing you learn about the storage allowance is the night every backup starts failing.
Tying it to the vault
The vault, GB quota and the encrypted-blob UI live on the server side. See the server documentation for browsing backups, downloading the raw encrypted blob, and managing storage.
You are reading the Laravel SDK v1.0 documentation.