Skip to content

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 as storage.Storage, auth.Repository, connections.Repository, connections.Prober, settings.Repository, targets.Repository, targets.Factory and audit.Repository; server, mcp and cmd/mongorescue are delivery adapters, storage, store, mongoconn and the tool runners are infrastructure adapters. Cookies and headers are handled in server; password, session, CSRF, throttling and scope rules live in auth. The REST API and the MCP server call the same operations service 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.Writer pipes from the tool process to storage and back; it is never read into a []byte or 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/http ServeMux with 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/bcrypt and the MongoDB Go driver. The driver is confined to the internal/mongoconn adapter (connection tests and database/collection discovery). The one exception is internal/oplog, which may import the driver's bson codec, 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 if internal/oplog imports another driver package. Dumps and restores always go through mongodump and mongorestore.
  • 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 (formatBytes in web/static/app.js). The documentation uses the same units.
  • Single binary. The dashboard (web/static) is embedded with //go:embed; server builds use CGO_ENABLED=0 and 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, desktop build 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
  1. The engine writes the MongoDB URI to a short-lived 0600 YAML file and passes --config=<file> to mongodump, so credentials never appear in the process list. The file is removed afterwards, and stale ones are cleaned up at startup.
  2. mongodump writes an archive to stdout. With encryption enabled, a goroutine copies it through the age writer into an io.Pipe.
  3. 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.
  4. 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.
  5. The backup record is saved to the store, a backup.succeeded or backup.failed event 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"]
  1. The target namespace is resolved: <db>_rescue_<timestamp>_<id> by default (an omitted safe_clone means true). An in-place restore into the source database or an explicit target_database requires safe_clone: false and confirm_in_place: true; otherwise the request is rejected with ErrInPlaceNotConfirmed (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.
  2. If the verify policy applies (always, or auto for in-place restores), the artifact is streamed once into io.Discard while its SHA-256 is compared with the record and, for encrypted backups, the age stream is fully authenticated. On any failure the restore stops before mongorestore starts.
  3. The artifact is streamed again, decrypted if needed, into mongorestore's stdin, and hashed on the way. Namespace rewriting (--nsFrom/--nsTo) implements safe clones; --nsInclude implements collection-level restores. --bypassDocumentValidation is added when the user holds that privilege on the target (checked through mongoconn).
  4. After mongorestore exits, 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 a mongorestore summary with documents that failed to restore (ErrDocumentsFailed), although mongorestore exits 0 then.
  5. The restore record is saved and a restore.succeeded or restore.failed event 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 the settings table, 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 of storage_targets, exactly one of them the default. The service builds one driver per target through the storage.NewForTarget factory, 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 its storage_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/config reads the deprecated environment variables and <data_dir>/config.json; internal/app imports 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/notify matches 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/metrics updates 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) plus users, sessions, api_keys (with their scope), settings audit_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 by audit.retention_days) and audit_chain_anchor (the last pruned entry's hash) with plain columns (a session belongs to its user with ON DELETE CASCADE). Each row stores the complete record as JSON in data, 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/migrations are embedded in the binary and applied at startup, each in its own transaction together with its schema_migrations row, 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). Each schema_migrations row 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 with ErrMigrationChanged, also before anything is written, so an edited released migration fails at startup instead of silently diverging. internal/store/upgrade_compat_internal_test.go builds 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 with go generate ./internal/restore, TEST ONLY keys) guard the restore pipeline against format regressions.
  • Transactions and concurrency. Connections use WAL, synchronous=NORMAL, foreign_keys=ON and a 5 second busy_timeout, and write transactions take the lock at BEGIN. 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 0600 before SQLite opens it, and the database, -wal and -shm files are forced to 0600 after opening, independently of the umask. secure_delete overwrites deleted content. An advisory lock on mongorescue.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 settings are always sealed with internal/secretbox before they are written (AES-256-GCM, random nonce, versioned sb2: 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 in settings makes startup fail with ErrSecretKeyMismatch when 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 and sb1: values (which were not bound to their location) left by earlier builds are re-sealed once, in the same transaction that records the secrets_format = sb2 marker in settings; from then on a secret field holding plaintext or an sb1: value (for example one planted while the server was stopped) makes startup fail with ErrUnsealedSecret, naming the table, record and field. A generated secret.key is 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 from MONGORESCUE_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_uri are 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.json from 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 to state.json.migrated-<timestamp>. On failure nothing is written and startup stops with the file untouched. Crash recovery (runs left in_progress are marked failed) runs afterwards in internal/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; /metrics needs an API key unless made public, and /mcp always 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/list by 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 HttpOnly and SameSite=Strict, and Secure over 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=Lax flow cookie; ID tokens are verified by go-oidc (RS256 and ES256 only) plus azp, iat and nbf checks; 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.CommandContext and 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, destructive drop_target requires explicit opt-in, and verify-before-restore guards in-place restores.