Encryption at rest¶
MongoRescue can encrypt every backup with age before it reaches storage. Encryption is streamed like the rest of the pipeline: mongodump output passes through the age writer on its way to the storage driver, so memory stays constant and the storage backend only ever receives ciphertext.
Modes¶
| Mode | Configure | Encrypts with | Decrypts with |
|---|---|---|---|
| X25519 (recommended) | recipients |
One or more public keys (age1…) |
Matching private key(s) in identity, or a retired identity |
| Passphrase (scrypt) | passphrase |
The passphrase | The same passphrase, or a retired one |
Encryption is configured under Settings → Encryption (or PUT /api/v1/settings, see api.md). Enabling it in x25519 mode requires at least one recipient, in passphrase mode a passphrase of at least 16 characters. Changes apply to the next backup.
Prefer X25519. Passphrase mode runs scrypt on every backup and restore, which costs about 256 MiB of RAM and roughly one second of CPU per operation. It also means the backup host holds the secret that decrypts every backup.
Generating a key¶
Generate key pair in the dashboard (POST /api/v1/settings/encryption/generate-key) creates a new X25519 key pair without storing it. The private key (AGE-SECRET-KEY-1…) is shown once, with Copy and Download (a standard age identity file):
Keep a copy of the private key somewhere safe and separate from the backups: without it, backups encrypted to its public key cannot be restored. Use this key stores the private key in MongoRescue's database (encrypted with the secret key) and adds the public key to the recipients. age-keygen works as well: paste the public key into Recipients and, where restores run, the private key into Identity.
Settings¶
| Setting | Description |
|---|---|
enabled |
Encrypt newly created backups |
mode |
x25519 or passphrase |
recipients |
age1… public keys new backups are encrypted to |
identity |
Private key(s), one per line, used by restores |
passphrase |
Passphrase for scrypt mode (encryption and decryption) |
retired_keys |
Read-only list of replaced or removed identities and passphrases |
identity and passphrase are stored encrypted, returned as ****** and never logged. Keys are validated when they are saved, so a malformed key is rejected with 400.
Retired keys. Replacing or removing the identity or the passphrase does not forget it: the old one is kept (encrypted) under retired_keys and still tried when restoring, so rotating keys never makes older backups unrestorable. Each retired passphrase that does not match costs one scrypt derivation during a restore.
Behaviour¶
- Storage keys of encrypted backups end in
.age. The recordedsize_bytesandsha256describe the stored ciphertext, not the plaintext dump. Backup records carryencryptedandencryption_mode(x25519orscrypt). - Layer detection. A stored backup is
mongodump --archiveoutput, optionally gzip-compressed (--gzip), optionally wrapped in age. Restores treat a backup as encrypted when its record says so, its key ends in.ageor its content starts with the age header, so ciphertext is never handed tomongorestoreas a dump. Compression is taken from the archive's own signature (falling back to a.gzkey suffix), so backups stored by earlier releases under a customtarget_key(no longer accepted) restore with the right--gzipflag. The first encrypted chunk is authenticated beforemongorestorestarts. - Backup-only instances. An instance configured with recipients but no identity can create encrypted backups but cannot restore them. This is intentional: the host that takes backups does not need the private key.
- Turning encryption off only affects new backups. Decryption uses the identity, the passphrase and every retired key regardless of
enabled, so existing encrypted backups stay restorable. - Existing unencrypted backups restore unchanged after encryption is enabled.
- A restore of an encrypted backup without a matching key fails with
422 Unprocessable Entitybeforemongorestorestarts; the message says which key to add under Settings → Encryption.
Encryption turned off by an upgrade¶
Releases up to v0.7.1 imported the deprecated encryption settings (MONGORESCUE_ENCRYPTION_*, encryption.* in config.json) without the on/off switch, so an installation that encrypted its backups before the upgrade has been writing unencrypted backups since. At startup MongoRescue detects this when the deprecated switch is still set (MONGORESCUE_ENCRYPTION_ENABLED=true or "enabled": true in config.json), the recipients or passphrase were imported, and encryption is off. Import records alone are not enough, because earlier releases wrote them for false as well. Keep the old setting in place until you have checked. MongoRescue then
- logs a warning on every start,
- shows a banner in the dashboard (Encryption was enabled in your previous configuration but is currently off; backups since the upgrade are NOT encrypted) with a button that opens Settings → Encryption, and
- sends one alert to every enabled notification channel, whatever the rules. The alert is sent again on the next start if the process stopped before it was handed to the notification service.
Encryption is not turned on automatically: check the recipients (or passphrase) and enable it yourself. The banner disappears once encryption is on, or when you dismiss it (POST /api/v1/settings/warnings/encryption_off_after_upgrade/dismiss). If you turn encryption off yourself in the settings, the warning is not raised. Backups taken while encryption was off stay unencrypted; take a new backup after enabling it, and delete the unencrypted ones if they must not stay in storage.
Key management and loss¶
A lost identity (or passphrase) means lost backups. age has no recovery mechanism, and MongoRescue keeps no copy of your private key outside its own database. If the identity an encrypted backup was made for is gone, that backup cannot be restored by anyone, including you: a restore fails with encryption: key material required (no key configured) or encryption: decryption failed (a different key), and mongorestore is never started.
- Escrow the private key offline, separately from the backups and from the MongoRescue host: a password manager, a hardware-backed vault, or a printed copy in a safe. Keep one copy per key you ever used, not just the current one. With X25519 you can also encrypt to a second recipient whose private key never leaves offline storage, so either key can restore.
- The database copy is not a backup of the key. The identity stored in MongoRescue's database is sealed with the secret key (
secret.keyorMONGORESCUE_SECRET_KEY, see production.md); losing the secret key or the data directory loses that copy too. - Rotating keys is safe. Replacing the identity or the passphrase retires the old one; restores keep trying retired keys, so backups made before a rotation still restore. Deleting the data directory, or starting a new instance without importing the old keys, drops the retired keys as well: paste every escrowed identity into Identity (one per line) on the new instance before restoring old backups.
- Test restores regularly on an instance that holds only the escrowed key, so a missing key is discovered before it is needed.
- Passphrase mode has the same property: a forgotten passphrase cannot be recovered or reset.
Key rotation runbook¶
Rotate a key when it may have leaked, when someone who held it leaves, or on a schedule. Every rotation below is audited (the request's audit entry carries key_rotated and the old and new fingerprints) and publishes a security.key_rotated event (action is secret_key, encryption or storage_credentials, detail the old and new fingerprints, approval_id the approval if any). No key material is ever logged, audited or sent. The dashboard has the three actions under Settings → Security → Key rotation.
secret.key¶
secret.key seals every credential in mongorescue.db: connection strings, the TLS client keys of connections and their passwords, storage credentials, notification secrets, job heartbeat URLs, the OIDC client secret, the audit webhook secret, the backup encryption identities and passphrases. POST /api/v1/security/rotate-secret-key (body {"current_password": "..."}) replaces it. It needs an administrator signed in to the dashboard who confirms their password, like the recovery kit; API keys are refused. With the two-person rule on it answers 202 and waits for a second administrator.
What happens:
- A marker (the old and new key fingerprints, never the keys) is written to the database, then the new key to
secret.key.next(temporary file, fsync, rename). - Every sealed value is re-sealed under the new key in one transaction, which also removes every dashboard session. It runs with
synchronous=FULL, so a power loss after it cannot bring the old key back. - The old key is written to
secret.key.previous,secret.key.nextis renamed tosecret.keyand the marker is removed.
A crash at any point is settled at the next start: when secret.key opens the database the rotation either completed or never committed (a leftover secret.key.next is discarded); when it does not and the marker names it as the old key, the commit happened and secret.key.next is installed. Any other key is refused as before.
Effects:
- Everyone signs in again. Sessions, and single sign-on flows in progress (their cookie is sealed with a subkey of
secret.key), end. - The old key no longer opens the database; the new one does.
- API keys keep working. Generated keys are SHA-256 digests and are not affected. Keys imported from the deprecated
MONGORESCUE_API_KEYare MACed with a subkey ofsecret.key: the old subkey is kept, sealed under the new key, so they keep verifying, and their first use after the rotation re-hashes them under the new subkey. - The recovery kit reminder fires again, because the kit's fingerprint changed. Download a new kit.
- Metadata snapshots taken before the rotation are sealed with the old key and stay below the old install ID's prefix (
_mongorescue/metadata/<old id>/); new snapshots go below the new key's prefix. They hold the credentials of their time sealed with the old, possibly leaked key, so they do not stay: the rotation records the old install ID in the same transaction, and once the delete grace period (Settings → Security) has passed since the rotation, the metadata backup service deletes the snapshots below that prefix on every storage target. Until then, restore an old snapshot with the old key: the previous recovery kit, or a kit downloaded with Include the previous key ("include_previous_key": trueinPOST /api/v1/recovery-kit), which addssecret.key.previous. - secret.key.previous goes after the old snapshots. Once they are pruned, MongoRescue deletes
secret.key.previousitself when a recovery kit was downloaded after the rotation; otherwise the dashboard warns (previous_secret_key) until you download a kit or delete the file.
With key wrapping on, the rotation is the same, on secret.key.wrapped, secret.key.next.wrapped and secret.key.previous.wrapped: each key is wrapped by the KMS (and checked) before it is written, so the rotation needs the KMS to be reachable, and no key of it reaches the disk in plain form. A new version of the KMS key itself needs no rotation of secret.key; another KMS key is reached with mongorescue key rewrap.
Keys from MONGORESCUE_SECRET_KEY¶
A key set by MONGORESCUE_SECRET_KEY cannot be rotated by MongoRescue (it does not own the environment); the endpoint answers 409. Rotate it by hand:
- Download a recovery kit, then stop MongoRescue.
- Unset
MONGORESCUE_SECRET_KEYand put the current key into<data dir>/secret.key(mode0600). - Start MongoRescue, sign in and rotate from the dashboard (or the endpoint).
- Either keep the file key, or stop again, set
MONGORESCUE_SECRET_KEYto the content of the newsecret.key, remove the file and start. - Download a new recovery kit.
Backup encryption key¶
POST /api/v1/encryption/rotate (admin) needs encryption to be on.
- X25519 mode: a new identity is generated; its public key replaces the old identity's public key among the recipients (other recipients, such as an offline escrow key, stay). The old identity is retired: restores keep trying it, so every older backup still restores. In recipient-only mode (no identity stored) MongoRescue holds no private key and refuses: generate a new pair yourself and replace the recipient.
- Passphrase mode: send the new passphrase,
{"passphrase": "..."}. The old one is retired. - Re-encrypting existing backups (optional,
{"reencrypt": true}): a background job streams every encrypted backup, decrypts it with the current or a retired key, encrypts it under the new key into a new object, reads that object back (its SHA-256, and a decryption whose plaintext hash must match the original's), then points the backup at it in one transaction. The old archive is not deleted at once: the same transaction stores a deleted copy of the record that keeps it until the delete grace period ends, when the purge removes it (and it can be undeleted until then). The job survives restarts: an interrupted backup's half-written object is removed and the backup processed again; finished ones are skipped.GET /api/v1/encryption/reencryptionreports its progress. A backup whose archive does not match its recorded SHA-256 is left alone and reported.
After rotating, escrow the new identity (download a new recovery kit) and keep the escrow of the old ones until no backup needs them.
Storage credentials¶
POST /api/v1/storage-targets/{id}/rotate-credentials (admin, body {"access_key_id": "...", "secret_access_key": "..."}) tests the new S3 credentials on a temporary object named .mongorescue-probe-rotate-<random> (below the target's prefix): write, read back, list and delete (the delete is checked). Only when every probe passes are they swapped in, in one write; otherwise it answers 422 with the steps and nothing changes. Revoke the old key at the provider afterwards. See production.md for the policy the new key needs.
Verify-before-restore¶
Before mongorestore runs, MongoRescue can stream the stored artifact once end to end: it recomputes the SHA-256 against the backup record and, for encrypted backups, decrypts the whole stream. Only if that pass succeeds does the real restore begin. On a checksum mismatch, a missing checksum or a decryption error, mongorestore is never started and the API returns 422 Unprocessable Entity.
Verification reads the artifact one extra time, so it is controlled by a policy:
| Source | Name | Values |
|---|---|---|
| Settings → General | restore_verify_policy |
always, auto (default), never |
| Request | "verify": true \| false on POST /api/v1/restore |
overrides the policy for that request |
In-place restores are always verified, whatever the policy or the request says: a damaged artifact or a wrong key is found before mongorestore touches an existing database. The artifact is read twice (verification, then the restore), never buffered. Only a backup record without a checksum (from an older release) skips the pass; the restore record then carries a warning. An explicit verify: true or the always policy still refuses such a record.
The policy therefore decides for safe clones only: auto and never stream straight into the new <db>_rescue_<timestamp>_<id> database, always (or verify: true) verifies first. A clone restore is refused before anything is written if that database already exists.
A clone restore that fails once mongorestore has started (a corrupted object, a wrong key discovered late, a checksum mismatch, a mongorestore error, a cancellation) drops the partially restored clone, and the record says so. The exception is a restore with documents that failed to insert: that clone is kept for inspection.
Every restore also checks the checksum while it streams, with or without verification: the stored bytes are hashed on their way into mongorestore, and once it exits the result is compared with the record. A mismatch fails the restore with backup checksum mismatch; a safe clone is then dropped, and the message says so. This catches damage that mongorestore itself accepts, such as a changed byte in the archive's collection or index metadata. Records from releases that did not store a checksum are restored without this check.
A restore also fails when mongorestore reports documents it could not insert (N document(s) failed to restore), for example duplicate keys in an in-place target that already held data without drop_target; mongorestore itself exits successfully in that case. The target, a clone too, is kept so the documents that arrived can be inspected. If mongorestore prints no document summary at all, the restore succeeds with the warning document counts unavailable. When the connection's MongoDB user holds the bypassDocumentValidation privilege on the target database (the built-in restore, dbAdmin, dbOwner and root roles grant it), restores run with --bypassDocumentValidation, so documents that predate a collection's validator are restored like the rest. A user with only readWrite restores with validation on: a document the validator rejects fails the restore with that hint, and granting the restore role fixes it.