Architecture¶
MongoRescue is a single Go binary that wraps the MongoDB Database Tools (mongodump, mongorestore) with scheduling, storage, encryption, notifications and a web dashboard. This page explains how the pieces fit together and why.
flowchart TB
UI["Web UI (embedded)"] --> API["REST API<br/>logging, CORS, session / API-key auth, CSRF, scopes"]
AI["AI assistants"] -- "Streamable HTTP /mcp<br/>or stdio bridge (mongorescue mcp)" --> MCP["MCP server<br/>API keys only, tool scopes,<br/>rate limit, audit"]
subgraph core["Core"]
OPS["operations<br/>backup, job and restore use cases"]
AUDIT["audit<br/>API key activity log"]
AUTH["auth<br/>setup, users, sessions, API keys,<br/>single sign-on rules"]
CONN["connections<br/>managed MongoDB servers"]
SCHED["Scheduler<br/>cron jobs, retention"]
BE["Backup engine"]
RE["Restore engine"]
BUS["Event bus"]
end
API --> AUTH
API --> CONN
API --> OPS
MCP --> OPS
MCP --> CONN
MCP --> AUDIT
OPS --> BE
OPS --> RE
OPS --> SCHED
CONN --> PROBE["mongoconn<br/>MongoDB Go driver: ping, list"]
PROBE <--> MONGO
SCHED -- resolve URI --> CONN
API --> SCHED
SCHED --> BE
OPS -. events .-> BUS
SCHED -. events .-> BUS
BE --> TOOLS["mongodump / mongorestore<br/>URI in a 0600 --config file"]
RE --> TOOLS
TOOLS <--> MONGO[("MongoDB")]
BE --> STORAGE["storage.Storage per storage target<br/>local disk or S3-compatible<br/>(AWS S3, MinIO, R2, B2)"]
RE --> STORAGE
SCHED -- retention --> STORAGE
API --> STORE["SQLiteStore<br/>mongorescue.db<br/>secrets sealed with secretbox"]
OPS --> STORE
AUDIT --> STORE
SCHED --> STORE
AUTH --> STORE
CONN --> STORE
BUS -.-> NOTIFY["notify<br/>Webhook, Telegram, Email, SMS"]
BUS -.-> METRICS["metrics<br/>served on GET /metrics"]
Design principles¶
- Hexagonal boundaries. Business logic (
backup,restore,scheduler,notify,auth,connections,settings,targets,operations,audit) does not know about HTTP, CLI flags or cloud SDKs. It depends on small interfaces (ports) such asstorage.Storage,auth.Repository,connections.Repository,connections.Prober,settings.Repository,targets.Repository,targets.Factoryandaudit.Repository;server,mcpandcmd/mongorescueare delivery adapters,storage,store,mongoconnand the tool runners are infrastructure adapters. Cookies and headers are handled inserver; password, session, CSRF, throttling and scope rules live inauth. The REST API and the MCP server call the sameoperationsservice for backups, job runs and restores, so the two adapters cannot apply different rules. - Stream-first I/O. Dumps can be hundreds of gigabytes. Data moves through
io.Reader/io.Writerpipes from the tool process to storage and back; it is never read into a[]byteor staged as a full copy on disk. Memory use does not grow with dump size. (Passphrase decryption is the one fixed exception: scrypt key derivation costs about 256 MiB, once per operation.) - Explicit dependency injection. No global singletons. Components receive their dependencies through constructors and functional options (
backup.WithEncryptor,restore.WithVerifyPolicy,server.WithMetricsHandler), which keeps every engine testable with fakes. - Standard library first.
net/httpServeMuxwith method patterns,log/slog,context,crypto. Third-party modules are limited to the AWS SDK v2,filippo.io/age,robfig/cron/v3, the Prometheus client,modernc.org/sqlite,golang.org/x/crypto/bcryptand the MongoDB Go driver. The driver is confined to theinternal/mongoconnadapter (connection tests and database/collection discovery). The one exception isinternal/oplog, which may import the driver'sbsoncodec, and nothing else from the driver, to parse and rewrite nested oplog entries; it never opens a connection (design). CI fails if any other production package imports the driver, or ifinternal/oplogimports another driver package. Dumps and restores always go throughmongodumpandmongorestore. - Sizes. The API reports sizes in bytes (
size_bytes,total_bytes). The dashboard shows them in binary (IEC) units, 1 KiB = 1024 bytes (B, KiB, MiB, GiB, TiB, PiB), with three significant digits and the UI language's number format; every size goes through one formatter (formatBytesinweb/static/app.js). The documentation uses the same units. - Single binary. The dashboard (
web/static) is embedded with//go:embed; server builds useCGO_ENABLED=0and cross-compile to Linux, macOS and Windows. The server serves the dashboard only with-dashboard/MONGORESCUE_DASHBOARD=true(set in the container image). The desktop app (cmd/mongorescue-desktop, Wails v2,desktopbuild tag) is the one CGO build: it serves the same handler in-process to a native webview, with no TCP listener; see desktop.md.
Package map¶
| Package | Responsibility |
|---|---|
cmd/mongorescue |
Entry point: bootstrap flags (-data-dir, -host, -port, -dashboard, -log-level, -version), logger setup, then hands off to app; mongorescue mcp runs the stdio MCP bridge and backup, restore, list, verify, status and help run the CLI instead |
cmd/mongorescue-desktop |
Desktop entry point (desktop build tag, Wails v2): runs app without a listener (App.Start/Stop) and serves App.Handler to the webview |
internal/app |
Wires dependencies, starts the HTTP server and scheduler (or, embedded, only the scheduler and background workers), coordinates graceful shutdown |
internal/desktop |
Desktop adapters without Wails: bootstrap config and log file, the session cookie jar and the same-origin fix for the webview's custom scheme |
internal/config |
Bootstrap options (data directory, listen address, dashboard switch, log level, secret key) and the reader for deprecated environment variables and <data_dir>/config.json (one-time import) |
internal/settings |
Dashboard-managed settings (general, security, encryption): validation, keep-secret rule, retired keys, the live snapshot engines and server read |
internal/targets |
Storage targets: validation, keep-secret updates, probe tests, default target, one cached driver per target |
internal/models |
Domain types: Job, Connection, BackupRecord, RestoreRequest, RestoreRecord, VerifyPolicy, ID validation |
internal/auth |
Setup mode and setup code, users (bcrypt), sessions with CSRF tokens, login throttling, API keys and their scopes (read < operator < admin), and the single sign-on rules (LoginOIDC: domain filter, group → role mapping, user creation, break-glass) |
internal/auth/oidc |
OpenID Connect protocol glue on github.com/coreos/go-oidc/v3 and golang.org/x/oauth2: discovery, PKCE and nonce, code exchange, ID token verification (RS256/ES256, azp, iat, nbf), claim extraction; no HTTP handlers. oidctest is the fake provider of the tests |
internal/operations |
Backup, job-run and restore use cases shared by the REST API and the MCP server: validation, safe-clone and in-place rules, restore preflights and post-restore verification (behind a RestoreInspector port), background runs, records and events; read models (Status, Stats) |
internal/runs |
The run Manager (background runs under the application lifecycle, per-database concurrency keys) and the run Registry (active runs: cancellation with who and why, live progress, their log files) |
internal/ha |
Active-passive high availability (High Availability): the lease manager (acquire, renew, step-down, epoch as fencing token, all timing on the instance's own monotonic clock), the instance state in ha.json, and the metadata snapshots of the active instance (write, retention, download and decryption for a takeover). internal/app runs the roles (RunHA): it reopens the App as active or standby when the lease changes hands, restores the newest snapshot before a takeover, and imports newer archives and catches missed schedules up after it |
internal/diskguard |
The data directory's space guard (Production Guide): the pre-run free-space check (an admission check of the run Manager), disk-full episodes (system.disk_full, the data_dir_full warning, runs refused until space is freed), and the final save of backup records, which fails a backup whose record cannot be saved and retries the save later |
internal/runlog |
Per-run log files under <data_dir>/logs: redacted, bounded lines streamed to disk, capped at 5 MiB with head and tail kept; tail reads and pruning |
internal/audit |
The activity log of API keys (MCP calls, REST requests): argument redaction, coalescing of repeated calls, pruning, listing |
internal/auditlog |
The audit log of every action (Audit Logging): hash chain and canonical encoding, verification, retention with the chain anchor, JSON Lines export and webhook forwarding |
internal/mcp |
MCP delivery adapter (official Go SDK): tools, resources and prompts, the scope/rate-limit/audit middleware, the Streamable HTTP handler and the stdio bridge |
internal/cli |
The CLI commands (backup, restore, list, verify, status; CLI Reference): flags (also after arguments), API key sources (never the server's MONGORESCUE_API_KEY), the in-place guard, the restore preflight, --wait polling, text and JSON output and the stable exit codes |
internal/apiclient |
Client of the REST API for the CLI: the origin-bound bearer transport (also used by the MCP stdio bridge), no redirects, envelope decoding, *APIError with sentinel errors per status, typed methods; it never imports server |
internal/connections |
Managed MongoDB connections: validation, keep-secret updates, tests, database/collection discovery |
internal/mongoconn |
The only production package that connects through the MongoDB Go driver: implements connections.Prober, lists collections for multi-collection backups, checks the restore and restore test privileges, the free disk space of a target and captures backup manifests (document counts, index specifications and the server version); for point-in-time recovery it reads the oplog window (hello.lastWrite.majorityOpTime, the oldest and newest entries, the replica set name and ID), copies a range (from, to] of local.oplog.rs as raw BSON to an io.Writer (memory bounded by one cursor batch, secondaryPreferred by default; each read proves on the member that serves it that the entry at from, with its term, still exists, else pitr.ErrOplogGap, and pitr.ErrOplogBehind when the member has not replicated up to to), looks up the term of one entry for the divergence check and checks find on local.oplog.rs (CanReadOplog); OplogSession keeps one client per stream |
internal/mongotls |
TLS configuration, custom certificate validation, and CA bundle caching for secure MongoDB connections |
internal/pitr |
Point-in-time recovery domain (PITR Architecture): streams, chains, oplog chunks and the collector state, BSON Timestamp/OpTime, and the Repository port implemented by store (migration 0021: pitr_streams, pitr_chains, oplog_chunks, pitr_state; a chunk and the collector's position are written in one transaction, and a chunk must continue the stored position), and PlanRestore, which picks the base and the unbroken run of chunks of a point-in-time restore and its --oplogLimit. The collector is internal/pitr/collector; the two-pass restore (base, then the filtered oplog through a synthetic archive on mongorestore stdin) is in internal/restore |
internal/secretbox |
AES-256-GCM encryption of credentials at rest and the secret key file |
internal/keywrap |
Root key protection: wraps secret.key using cloud KMS providers (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault Transit) |
internal/keyrotation |
Online rotation of secret.key: re-seals all stored secrets in an atomic transaction and manages interrupted rotations |
internal/backup |
Backup engine: runs mongodump, hashes, optionally encrypts, streams to storage, cleans up on failure |
internal/restore |
Restore engine: safe-clone namespace mapping, verify-before-restore, decryption, runs mongorestore |
internal/postrestore |
Post-restore orchestration: validates and plans post-restore script execution on safely cloned databases |
internal/copies |
Secondary storage target replication (3-2-1 backup copies): background archive copying, integrity verification, and retry queue |
internal/metabackup |
Self-backup service: online SQLite VACUUM INTO snapshots sealed with age encryption and stored under _mongorescue/metadata/ |
internal/recoverykit |
Emergency recovery kit generator: produces encrypted tarballs containing secret.key, age identities, and restore instructions |
internal/reencrypt |
Background re-encryption: re-encrypts stored backups under updated age keys without buffering, guarded by safety tombstones |
internal/readiness |
RPO/RTO readiness evaluator: checks backup freshness against Recovery Point Objectives and monitors key escrow status |
internal/scheduler |
Cron scheduling (robfig/cron/v3) and retention pruning after successful scheduled runs (on-demand runs never prune); PlanRetention is shared by the prune and its dry-run preview, and pinned and last verified backups are kept |
internal/verify |
Streams a stored archive through SHA-256 (optionally rate limited, optionally decrypting the age stream) and compares it with the record |
internal/integrity |
Integrity sweep, on-demand verification, automated restore tests into <db>_rescue_verify_<ts> and storage scans with orphan import (Verification & Testing); a business package behind ports for the store, the restore engine, the MongoDB adapter and the storage targets |
internal/storage |
The Storage port and its drivers: local filesystem and S3-compatible (AWS SDK v2 multipart) |
internal/store |
Metadata persistence (jobs, history, users, sessions, API keys, connections, notification settings) in an embedded SQLite database; data directory lock |
internal/encryption |
Streaming age encryption and decryption (X25519 and scrypt), key generation, identity hygiene |
internal/events |
Domain events emitted when backups and restores finish, and the in-process event bus |
internal/throttle |
A context-aware token-bucket reader with an injectable clock; the backup engine caps the upload stream with it (max_upload_mbps, Production Guide) |
internal/heartbeat |
Outbound heartbeat pings to a dead-man's switch (Monitoring & Alerts): the global ping every monitoring.heartbeat_interval while the scheduler is healthy, and per job <url>/start, <url> and <url>/fail (nothing after the start of a cancelled or interrupted run) as a scheduler.RunObserver; fire-and-forget, retried, in order per job, through the guarded notification HTTP client, never logging a URL |
internal/notify |
Notification channels (webhook, Telegram, SMTP, Twilio), rules, and the asynchronous delivery dispatcher, which stores every delivery in the durable outbox (notification_outbox, in internal/store) before it is sent: at least once, in order per channel, retried across restarts |
internal/metrics |
Prometheus metrics on a dedicated registry |
internal/server |
REST API, authentication, scope (route → scope table) and CORS middleware, the /mcp mount with its Origin and Host checks, embedded dashboard serving |
internal/redact |
Dependency-free helpers that scrub credentials from URIs and free text |
internal/logsafe |
Structured log sanitization: strips control codes and terminal escape sequences to prevent log injection |
internal/ratelimit |
In-memory token buckets per key (API key, session, client address) with bounded memory, shared by the REST API rate limit and the MCP limiter, and a once-per-interval gate for log lines |
internal/sharedcache |
A single-flight cache with a TTL, a change stamp and invalidation, keyed by what a value depends on (such as the caller's connection access): the operations service shares the overview history and statistics through it, the readiness service its report; the store's change stamp (migration 0029) counts every write to the tables they read |
internal/diskspace |
Free space of a local filesystem (statfs, GetDiskFreeSpaceExW on Windows) for restore preflights of a server on the same host |
internal/mongouri |
Structural validation of MongoDB connection strings without the driver |
internal/mongotools |
Helpers shared by the tool runners, chiefly passing the URI through a private --config file |
internal/oplog |
Groundwork for point-in-time recovery (PITR Architecture): the Scanner that walks oplog chunks as they stream (entry count, first and last ts/t), the namespace Filter that rewrites oplog entries for a safe-clone or single-database replay (refusing renames across the selection and unknown commands, ending before --oplogLimit), and the ArchiveWriter of the synthetic oplog-only archive for mongorestore --oplogReplay. Uses the driver's bson codec only, never a connection |
internal/update |
Non-intrusive update checker querying GitHub releases for new versions |
internal/integration |
integration-tagged end-to-end tests against real tools, MongoDB and S3 providers |
web |
Embedded dashboard assets: index.html, style.css (theme tokens) and nav.css, i18n.js, app.js and its modules (runs.js run control, trust.js integrity, forms.js forms and accessibility, bulk.js selection and bulk actions, nav.js command palette, shortcuts, deep links and toasts, overview.js the Overview tab, widgets.js the cron builder and retention presets, tables.js and tables.css table filters, sorting, saved views, columns, skeletons and form validation) |
Backup data flow¶
sequenceDiagram
participant C as Scheduler or API
participant E as Backup engine
participant D as mongodump
participant X as age (optional)
participant S as Storage
C->>E: Run(ctx, options)
E->>E: write URI to a 0600 --config file
E->>D: start with --archive [--gzip]
E->>S: Save(ctx, key, reader)
loop until EOF
D-->>X: archive bytes on stdout
X-->>S: stored bytes, hashed (SHA-256) and counted in flight
end
alt dump, encryption or upload failed, or ctx cancelled
E->>D: kill process
E->>S: Delete(key)
E-->>C: record with status failed
else success
E-->>C: record with size and SHA-256
end
C->>C: save record, publish backup.succeeded or backup.failed
Note over C: after a scheduled run, the scheduler applies retention
- The engine writes the MongoDB URI to a short-lived
0600YAML file and passes--config=<file>tomongodump, so credentials never appear in the process list. The file is removed afterwards, and stale ones are cleaned up at startup. mongodumpwrites an archive to stdout. With encryption enabled, a goroutine copies it through the age writer into anio.Pipe.- The (possibly encrypted) stream is hashed and counted as it is read by
Storage.Save. The recorded SHA-256 and size therefore describe the stored bytes. - Errors propagate in both directions: a failed upload stops the dump instead of blocking on a full pipe, and a failed dump fails the upload. On any failure or cancellation the partially written object is deleted, so no truncated artifact is ever recorded as a backup.
- The backup record is saved to the store, a
backup.succeededorbackup.failedevent is published, and, after a scheduled (cron) run only, the scheduler applies retention; on-demand runs and manual backups never prune.
Restore data flow¶
flowchart TD
REQ["Restore request"] --> INP{"in place?"}
INP -- "no (default)" --> T1["target: #lt;db#gt;_rescue_#lt;timestamp#gt;_#lt;id#gt;"]
INP -- yes --> CONF{"confirm_in_place?"}
CONF -- no --> REJ["rejected:<br/>ErrInPlaceNotConfirmed"]
CONF -- yes --> T2["target: target_database<br/>or the source database"]
T1 --> VER{"verify?"}
T2 --> VER
VER -- yes --> V["verify pass: Storage.Retrieve,<br/>SHA-256 and age decryption<br/>into io.Discard"]
V --> OK{"checksum and<br/>decryption OK?"}
OK -- no --> ABORT["restore fails,<br/>target untouched"]
OK -- yes --> R["Storage.Retrieve"]
VER -- no --> R
R --> DEC["age decryption,<br/>if encrypted"]
DEC --> MR["mongorestore --archive<br/>--nsFrom / --nsTo"]
MR --> REC["save record,<br/>publish restore event"]
- The target namespace is resolved:
<db>_rescue_<timestamp>_<id>by default (an omittedsafe_clonemeans true). An in-place restore into the source database or an explicittarget_databaserequiressafe_clone: falseandconfirm_in_place: true; otherwise the request is rejected withErrInPlaceNotConfirmed(HTTP 400). The source part of a clone name is shortened so that it fits MongoDB's 63-byte limit. A client-supplied target database and selected collections must be valid MongoDB names (models.ErrInvalidNamespace, HTTP 400); the backup's own database name is never refused. - If the verify policy applies (
always, orautofor in-place restores), the artifact is streamed once intoio.Discardwhile its SHA-256 is compared with the record and, for encrypted backups, the age stream is fully authenticated. On any failure the restore stops beforemongorestorestarts. - The artifact is streamed again, decrypted if needed, into
mongorestore's stdin, and hashed on the way. Namespace rewriting (--nsFrom/--nsTo) implements safe clones;--nsIncludeimplements collection-level restores.--bypassDocumentValidationis added when the user holds that privilege on the target (checked throughmongoconn). - After
mongorestoreexits, the unread rest of the artifact is hashed and the SHA-256 compared with the record: a mismatch fails the restore (backup checksum mismatch), and so does amongorestoresummary with documents that failed to restore (ErrDocumentsFailed), althoughmongorestoreexits 0 then. - The restore record is saved and a
restore.succeededorrestore.failedevent is published.
Settings and storage targets¶
Only bootstrap options (data directory, listen address, dashboard switch, log level, optional secret key) come from flags or the environment. Everything else is dashboard-managed and lives in the database:
- Settings (
internal/settings) are stored one row per key in thesettingstable, secrets sealed with secretbox. The service validates every change, persists only the keys that changed and swaps an in-memory snapshot, and rebuilds the age encryptor and decryptor (current plus retired keys). Engines, the scheduler, auth and the HTTP server read that snapshot for each operation or request (backup.WithRunConfig,restore.WithRunConfig,auth.WithSessionPolicy,server.WithSettings), so no restart is needed. - Storage targets (
internal/targets) are rows ofstorage_targets, exactly one of them the default. The service builds one driver per target through thestorage.NewForTargetfactory, caches it by target and version, and rebuilds it when the target changes. The backup engine writes to the target named in the backup options (the default when none), and every backup record stores itsstorage_target_id: restores, deletions and retention use that target, never the current default. The store refuses to delete a target a job or a completed or running backup still references. - One-time import. At startup
internal/configreads the deprecated environment variables and<data_dir>/config.json;internal/appimports each source once (settings without a stored value, a storage target when none exists, the old default connection and static API key), records it as imported and warns.
Storage interface¶
Every storage target implements one small, mockable port (internal/storage/storage.go):
type Storage interface {
Save(ctx context.Context, key string, r io.Reader) (*models.StorageObject, error)
Retrieve(ctx context.Context, key string) (io.ReadCloser, error)
Delete(ctx context.Context, key string) error
List(ctx context.Context, prefix string) ([]*models.StorageObject, error)
Stat(ctx context.Context, key string) (*models.StorageObject, error)
}
Save must stream without buffering the whole input. The local driver validates every key against its root directory to prevent path traversal and writes through a temporary file that is renamed on success. The S3 driver uses the multipart uploader and sends integrity checksums only when required, which keeps it compatible with R2, B2 and MinIO. New drivers must pass the shared conformance suite in internal/integration.
Events, notifications and metrics¶
When a backup or restore finishes, the scheduler or API server publishes a domain event to an in-process bus (internal/events). Publish never blocks: if the bus queue is full the event is dropped and counted in mongorescue_events_dropped_total. Two subscribers consume events:
internal/notifymatches events against rules and stores one delivery per channel in the durable outbox (notification_outbox) in one transaction; a dispatcher hands the oldest due delivery of each channel to a fixed worker pool (10 second per-attempt timeout, exponential-backoff retries, then rounds rescheduled in the outbox), and removes it once delivered or given up. Deliveries left by a crash are sent at the next start; only the few milliseconds between an action and its event reaching the outbox through the bus are not covered.internal/metricsupdates Prometheus counters, histograms and gauges.
Both are started and stopped by internal/app with the rest of the process, so no goroutine outlives shutdown.
Metadata persistence¶
internal/store keeps jobs, backup and restore records, users, sessions, API keys, connections, storage targets, settings, and notification channels and rules in an embedded SQLite database, always <data_dir>/mongorescue.db. The driver, modernc.org/sqlite, is pure Go, so release binaries stay CGO_ENABLED=0 and self-contained.
- Schema. One table per entity (
jobs,backups,restores,connections,notification_channels,notification_rules) plususers,sessions,api_keys(with theirscope),settingsaudit_log(API key activity, pruned to the newest 10,000),audit_events(the hash-chained audit log of every action, append-only through triggers, pruned byaudit.retention_days) andaudit_chain_anchor(the last pruned entry's hash) with plain columns (a session belongs to its user withON DELETE CASCADE). Each row stores the complete record as JSON indata, which is what the store reads back, so model fields can be added without a migration. Columns used for filtering and ordering (ID, name, database, job ID, status, start time) are copies rewritten on every save and indexed; lists come back newest first (backups, restores) or by name (jobs, channels, rules). - Migrations. Versioned SQL files in
internal/store/migrationsare embedded in the binary and applied at startup, each in its own transaction together with itsschema_migrationsrow, so reopening an up-to-date database is a no-op. A database migrated by a newer release is refused rather than downgraded (ErrSchemaTooNew, before anything is written). Eachschema_migrationsrow records the SHA-256 checksum of the migration's embedded SQL (rows applied before migration 0014 are backfilled from the running binary); a database whose applied migration no longer matches the binary, or is missing from it, is refused withErrMigrationChanged, also before anything is written, so an edited released migration fails at startup instead of silently diverging.internal/store/upgrade_compat_internal_test.gobuilds a fixture for every earlier schema version, inserting the rows each release wrote after each migration, and checks that opening it preserves every field; a new migration must add its step there. Golden backup artifacts in every stored layout (internal/restore/testdata/compat, regenerated withgo generate ./internal/restore, TEST ONLY keys) guard the restore pipeline against format regressions. - Transactions and concurrency. Connections use WAL,
synchronous=NORMAL,foreign_keys=ONand a 5 secondbusy_timeout, and write transactions take the lock atBEGIN. The pool holds a single connection, which serialises statements inside the process; multi-step operations (deleting a channel together with its references in rules, the legacy import) run in one transaction. - Files. The database is created with mode
0600before SQLite opens it, and the database,-waland-shmfiles are forced to0600after opening, independently of the umask.secure_deleteoverwrites deleted content. An advisory lock onmongorescue.lock(flock / LockFileEx) keeps a second instance off the same data directory. - Secrets at rest. Connection URIs, the post-restore commands of connections (an erasure log, also while a change of them waits for an approval), notification channel secrets (webhook URL, HMAC secret and header values, bot token, SMTP password, Twilio token), S3 secret keys of storage targets and the encryption identity, passphrase and retired keys in
settingsare always sealed withinternal/secretboxbefore they are written (AES-256-GCM, random nonce, versionedsb2:prefix; the associated data binds each value to its table, record ID and field, so a ciphertext copied into another record or field does not open), and opened when read, so the domain packages only ever see plaintext. Reads refuse secret fields that are not sealed in the current format. A key check value insettingsmakes startup fail withErrSecretKeyMismatchwhen the key is missing or wrong; only the secret fields are inspected to decide whether a database without one already holds encrypted values. Plaintext values andsb1:values (which were not bound to their location) left by earlier builds are re-sealed once, in the same transaction that records thesecrets_format = sb2marker insettings; from then on a secret field holding plaintext or ansb1:value (for example one planted while the server was stopped) makes startup fail withErrUnsealedSecret, naming the table, record and field. A generatedsecret.keyis fsynced together with its directory before the database records the key check value. Passwords are bcrypt hashes; session tokens and generated API keys (random, 256 and 160 bits) are stored as SHA-256 hashes, and a key imported fromMONGORESCUE_API_KEY(chosen by an administrator) as an HMAC-SHA256 under an HKDF subkey of the secret key (secretbox.DeriveSubkey), so a copy of the database alone does not allow guessing it offline; keys imported by earlier releases keep their SHA-256 hash and still verify. - Legacy connection strings. Jobs that still carry a
mongo_uriare migrated at startup into one connection per distinct URI (named after its hosts), and their backup records get the connection ID and name. - Legacy import. When
state.jsonfrom an older release exists and the database is empty, it is imported in one transaction, the row counts are verified, and the file is renamed tostate.json.migrated-<timestamp>. On failure nothing is written and startup stops with the file untouched. Crash recovery (runs leftin_progressare marked failed) runs afterwards ininternal/app, so it covers imported records too.
The store serves a single MongoRescue instance; the roadmap includes a pluggable metadata store for multi-instance deployments.
Security boundaries¶
- Everything under
/api/except health, setup status, setup, login and the sign-in methods needs a session cookie or an API key;/metricsneeds an API key unless made public, and/mcpalways needs an API key (sessions are never accepted there). Cookie-authenticated unsafe requests must carry the session's CSRF token. Every request has an effective scope, checked against one route table: a session gets the scope of its user's dashboard role (viewer read, operator operator, admin admin), an API key its own scope capped by its creator's current role (see design/roles.md). - The MCP server exposes no tool that deletes, restores in place or reconfigures anything, filters
tools/listby the key's scope and checks the scope again on every call, rate limits each key and records every call in the audit log with redacted arguments. There is no unauthenticated mode; a fresh instance is in setup mode until the first user is created with the one-time code from the logs. - Session cookies are
HttpOnlyandSameSite=Strict, andSecureover TLS or behind a trusted proxy. Login attempts are reserved atomically before the password check and throttled per (IP, username), with a per-IP failure count that tightens the budget but never blocks a correct password; every login costs exactly one bcrypt comparison whether or not the user exists, public POSTs require a JSON body and a same-origin (or allowed)Origin, deleting a user revokes their API keys, and API keys and the imported static key are compared through their SHA-256 digest or HMAC in constant time. - Single sign-on (
/auth/oidc/start,/auth/oidc/callback, not in the desktop app) uses the authorization code flow with S256 PKCE, a nonce and a single-use state kept in a sealed,SameSite=Laxflow cookie; ID tokens are verified by go-oidc (RS256 and ES256 only) plusazp,iatandnbfchecks; users are identified by issuer and subject only, never linked by name or email; all provider traffic goes through the guarded HTTP client of the notifications (timeout, no redirects, no link-local or metadata addresses) with responses capped at 1 MiB. A local administrator always remains as the break-glass way in. - MongoDB URIs are redacted (
internal/redact) before logging, error messages and API serialization; other secrets are masked in API responses. - Tools are started with
exec.CommandContextand separate argument slices; nothing is passed through a shell, and cancelling the context terminates the child process. - Restores default to safe clones in both the API and the dashboard, in-place restores require
confirm_in_place: true, destructivedrop_targetrequires explicit opt-in, and verify-before-restore guards in-place restores.