fix: document inspect and --confirm-db's actual behaviour

This commit is contained in:
2026-09-07 14:45:37 +00:00
parent d83061786c
commit be299845ca
2 changed files with 81 additions and 13 deletions
+43 -5
View File
@@ -606,11 +606,49 @@ There is no code path that upserts an archive's documents over existing ones:
merging two control planes' data reconciles nothing about which SSH keys are
still valid or which users still exist, and an upsert would resurrect a
revoked key or a deleted member from the older side. `--force` drops each
collection in the archive first, and is gated behind a typed confirmation
(the target database's name, typed back) on a terminal, or `--confirm-db NAME`
matching the target exactly with none. Naming the target in the command itself
means a copied command carries its intended target with it and cannot destroy
a different one by accident.
collection in the archive first, and is gated behind a second assurance:
`--confirm-db NAME` matching the target exactly, which works everywhere, or —
on a terminal only, and only when `--confirm-db` was not given — the target
database's name typed back at a prompt. `--confirm-db` is accepted on a
terminal too: it is the stronger of the two, because naming the target in the
command itself means a copied command carries its intended target with it and
cannot destroy a different one by accident. Without a terminal and without
`--confirm-db`, `--force` is refused.
**`--force` drops only what the archive names.** Collections already in the
target that the archive does not carry are left untouched and **named in a
warning** — an archive taken with `--exclude workflow_log_lines` restored over
a live database leaves the old lines joined to restored runs, which the
operator must be told. Dropping them instead would delete data nobody asked to
delete, and there is no way back from that.
**Index specifications are replayed verbatim, never reconstructed.**
`dumpIndexes` stores each spec as extended JSON over the raw BSON the server
reported, and `replayIndexes` hands it back to `createIndexes` through
`RunCommand` with only `v` and `ns` stripped and `_id_` skipped. Rebuilding a
`mongo.IndexModel` from a hand-picked set of options dropped
`partialFilterExpression` — which this codebase relies on in
`services/workflows.go` and `services/settings.go` — so a partial unique index
came back as a full one, failed on duplicate keys, and aborted the restore
mid-write. Reconstructing the key document from JSON also lost compound key
order, which is significant.
**`backup.ciphertextFields` mirrors `server/internal/models` by hand.**
`shared/` is a separate module and `models` is under `server/internal`, so
`shared/backup` cannot import it; the map naming each collection's `*_enc`
fields (`keys`, `secrets`, `auth_providers`, `console_sessions`) must change in
the same commit as any of those bson tags, the same hazard as
`web/lib/targets.ts` and `services.MaxWorkloadLogLines`. Wrong field names are
silent: `verify`'s live probe simply finds no ciphertext and reports "this
database stores no ciphertext yet", so the one gate that catches what a
fingerprint cannot no-ops. `settings` is deliberately in neither that map nor
`CiphertextCollections()` — its ESO read token is a SHA-256 hash, not
ciphertext.
**A file-backed `backup` writes to `<name>.tar.gz.partial` and renames on
success**, the same discipline the agent uses for `authorized_keys`. A failed
dump must not leave a partial file named exactly like a good archive; `--out -`
is untouched, since a broken pipe has no file to mislead anyone.
**`vantagectl/Dockerfile`'s runtime stage is `scratch`, and needs the same
explicit `/tmp` as `server/Dockerfile`.** `restore` extracts an archive to a
+38 -8
View File
@@ -130,6 +130,25 @@ Each line of output answers a different question:
it worth putting on a schedule — a backup job that "succeeded" last night is
not the same claim as a backup that will actually restore.
## Looking inside an archive
`inspect` prints an archive's manifest and touches no database at all — no
`--mongo-uri`, no key. It is what to run against an archive of unknown origin,
before deciding whether it is the one you want:
```bash
vantagectl inspect /backups/vantage-backup-vantage-20260907T020000Z.tar.gz
```
It reports when the archive was taken and on which host, the Vantage and
MongoDB versions behind it, the database it came from, the key fingerprint (or
that it carries none), every collection with its document count and size, and
anything `--exclude` left out. Opening the archive verifies every member's
checksum on the way, so a corrupt archive fails here too.
Reach for `verify` instead when the question is whether the key you hold opens
it; reach for `inspect` when the question is what it is.
## Restoring
`restore` expects the target database to be empty. Pointed at one that already
@@ -143,13 +162,25 @@ vantagectl restore /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \
```
To overwrite a database that is not empty, add `--force`, which drops each
collection named in the archive before loading it. On a terminal, `--force`
alone prompts you to type the target database's name back — a deliberate pause
before something destructive. With no terminal — a Kubernetes Job, a CI step, a
cron entry — that prompt cannot happen, so `--force` instead requires
`--confirm-db NAME` naming the target exactly; a mismatch is refused. Naming
the database in the command itself means a copy-pasted invocation carries its
intended target with it and cannot destroy a different one by accident.
collection named in the archive before loading it. `--force` always needs a
second assurance, in one of two forms:
- `--confirm-db NAME`, naming the target exactly. A mismatch is refused. This
works everywhere — on a terminal and in a Kubernetes Job, a CI step or a cron
entry alike — and is the form to script.
- Nothing, on a terminal: `--force` alone prompts you to type the target
database's name back, a deliberate pause before something destructive.
Without a terminal and without `--confirm-db`, `--force` is refused: there is
nobody there to prompt. Naming the database in the command itself means a
copy-pasted invocation carries its intended target with it and cannot destroy a
different one by accident.
`--force` drops only the collections the archive carries. Anything else already
in the target is left alone and named in a warning, so an archive taken with
`--exclude workflow_log_lines` restored over a live database tells you the old
log lines are still there, joined to freshly restored runs. Dropping them
instead would delete data you never asked to delete.
`restore` also refuses when the archive's key fingerprint does not match the
`KEY_ENCRYPTION_KEY` in your environment — see "When the key is wrong" below.
@@ -187,7 +218,6 @@ will come back with ciphertext nobody can read:
- `secrets` — the vault
- `auth_providers` — OIDC/SSO client secrets
- `console_sessions` — RDP/VNC credentials
- `settings` — anything encrypted at the instance level
There is no way to recover that ciphertext afterwards. If you have reached
this point, the right key was lost along with the chance to read those rows —