Implements the mongodb-superpowers-package design: Stable Tasks 1-50 and Advanced Tasks 1-15. The design assumes 19 Stable + 12 Advanced Gradle projects under modules/mongodb*. This repository's fail-closed registry declares exactly 19 leaf identities, so those modules become package boundaries inside the registered leaf :adapter:outbound:persistence-mongo, with the design's module dependency table enforced by ten ArchUnit rules. The mapping and every deviation are recorded in docs/mongodb/repository-adaptation.md. Contract highlights, all enforced by tests rather than convention: - Transaction body retry and commit retry are separate loops. A new session per body attempt; commit-only retry on an unknown commit. The body is never replayed after a commit ambiguity, so a failover cannot become a duplicate. - MongoExecutionOutcome keeps both ambiguous outcomes distinct from success and failure, and MongoFailureContext records only the design-permitted fields. - Failure classification reads server error labels before numeric codes. - BSON representations come from a pinned manifest, never a library default, and a golden type-signature gate fails on any drift. - Index and validator changes go through the manifest and the admin plane; metadata ownership gates every drop. - Every Advanced capability refuses construction unless its flag is enabled. Verified against real servers, not only unit tests. Running the lanes for the first time exposed four defects that a green `check` had hidden: - Four release lanes passed while executing zero tests; the gate now counts executed tests per lane and fails on zero. - The "single replica set" fixture was a standalone, because Testcontainers 2.x needs withReplicaSet(); its test only asserted a connection string. - The three-node fixture was three independent clusters, so no election could occur, and awaitNewPrimary() compared against the post-stop primary. - The migration lease checked modifiedCount, so a same-millisecond refresh read as a lost lease. scripts/verify-mongodb-platform.sh now reports: 9 lanes, 0 skipped, 0 failed, every evidence category produced. scripts/verify-mongodb-advanced.sh reports NOT PROMOTABLE: actual-topology evidence (real sharded cluster, real KMS, real target deployment) is unobtainable here, so it is named rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
148 lines
7.2 KiB
Markdown
148 lines
7.2 KiB
Markdown
# Security and Observability
|
|
|
|
Design §26–§28, decision D-05. The application plane and the admin plane are different credentials on
|
|
different clients, and telemetry never becomes an exfiltration path.
|
|
|
|
## 1. Roles
|
|
|
|
`MongoPrincipalRole` — one credential per role, least privilege:
|
|
|
|
| Role | Grants |
|
|
|---|---|
|
|
| `APP_READ` | `find` on allowlisted collections |
|
|
| `APP_WRITE` | `insert`, `update`, `delete` on allowlisted collections |
|
|
| `CHANGE_STREAM` | `changeStream`, `find` |
|
|
| `MIGRATION` | index and validator management on the target collections |
|
|
| `SEARCH_ADMIN` | search index management |
|
|
| `SHARD_ADMIN` | shard key operations |
|
|
| `ENCRYPTION_ADMIN` | key vault access |
|
|
| `DBA` | the human plane; never used by an application |
|
|
|
|
`MongoSecurityProfileValidator` checks the profile at startup. `forbiddenPrivilegesHeld()` names the
|
|
privileges the profile holds and must not — the validator reports *which* one, because "your
|
|
credential is over-privileged" without a name is an unactionable finding.
|
|
|
|
The privileges that must never appear on an application credential: `dropDatabase`,
|
|
`dropCollection`, `shutdown`, `killop`, `root`, `__system`, `dbOwner`, `userAdminAnyDatabase`.
|
|
|
|
## 2. Credentials are references, not values
|
|
|
|
`MongoCredentialReference` holds a `secret://…` reference plus the role. The reference is resolved at
|
|
connection time by the secret provider; the password is never a property value, a log field, or a
|
|
constructor argument that could end up in a stack trace.
|
|
|
|
`MongoCredentialRotationPolicy` states the rotation contract: overlapping validity, a drain window,
|
|
and a rotation that never requires a restart. `MongoClientGenerationRegistry` implements the swap —
|
|
a new `MongoClientGeneration` starts serving new operations while the previous generation is
|
|
`markDraining()` until its in-flight operations finish. Killing the old client immediately fails every
|
|
in-flight request, which is why rotation without generations is an outage.
|
|
|
|
Rotation is a failover scenario in the release gate (`MongoFailoverScenario.CREDENTIAL_ROTATION`),
|
|
not a runbook step people hope works.
|
|
|
|
## 3. TLS and connection policy
|
|
|
|
`MongoSecurityProfile.production(...)` requires TLS and refuses `tlsAllowInvalidCertificates` /
|
|
`tlsAllowInvalidHostnames`. `MongoSecurityProfile.local(...)` exists so a developer does not have to
|
|
weaken the production factory to get a container to connect; the startup validator refuses a local
|
|
profile on a production runtime profile.
|
|
|
|
## 4. Admin plane (D4)
|
|
|
|
`MongoAdminGateway` is the only path to `MongoAdminOperation`, and it runs on the D4 client with the
|
|
DBA-scoped credential — not the application's.
|
|
|
|
- `MongoAdminAuthorization` checks the caller's role against the operation.
|
|
- `MongoAdminRuntimeGuard` refuses high-risk operations (`highRisk()`) unless the runtime profile
|
|
explicitly permits them; a `dropCollection` reachable from a running application is a data-loss
|
|
vector regardless of how well-reviewed the calling code is.
|
|
- `MongoAdminAuditRecord` records who ran what, when and against which collection profile — before
|
|
execution, so a failed attempt is recorded too.
|
|
|
|
The native capability gateway (D3) refuses any admin-category command, so there is no path from the
|
|
application plane into the admin plane.
|
|
|
|
## 5. Observability tags
|
|
|
|
`MongoObservationConvention` allowlists exactly eight tag names:
|
|
|
|
```
|
|
mongoProfile, databaseProfile, collectionProfile, operationName,
|
|
operationType, result, failureCategory, consistencyProfile
|
|
```
|
|
|
|
and explicitly forbids:
|
|
|
|
```
|
|
documentId, rawTenantId, tenantId, dynamicCollectionName, queryParameter,
|
|
query, fullBson, resumeToken, shardKeyValue, plaintextPII, credential
|
|
```
|
|
|
|
Two reasons, and both matter. Cardinality: a tag whose values are document ids produces one time
|
|
series per document, which is how a metrics backend falls over. Confidentiality: a metric label is
|
|
stored, shipped and retained by systems with a different access model than the database.
|
|
`requireAllowed(tagName)` throws on anything outside the list, so a new tag is a deliberate change to
|
|
the convention rather than a line in a service.
|
|
|
|
`MicrometerMongoOperationObserver` implements the `MongoOperationObserver` port;
|
|
`NoOpMongoOperationObserver` is the default so observation is opt-in and never a hard dependency.
|
|
|
|
## 6. Driver-native listeners
|
|
|
|
`MongoDriverObservabilityConfiguration` registers three driver listeners, because they answer
|
|
questions the application-level timer cannot:
|
|
|
|
| Listener | Answers |
|
|
|---|---|
|
|
| `MongoCommandObservationListener` | How long did the *server* take, versus how long the caller waited? |
|
|
| `MongoPoolObservationListener` | Was the wait time connection checkout rather than query execution? |
|
|
| `MongoSdamObservationListener` | Did the topology change — an election, a node removed — during the window? |
|
|
|
|
Without pool and SDAM events, every failover looks like "the database got slow", and the difference
|
|
between "we need a bigger pool" and "we lost a primary" is invisible.
|
|
|
|
## 7. Command redaction
|
|
|
|
`MongoObservationRedactor.describe(commandName)`:
|
|
|
|
- Authentication and user-management commands (`authenticate`, `saslStart`, `saslContinue`,
|
|
`getnonce`, `createUser`, `updateUser`, `copydb*`) render as `<redacted>` — their arguments carry
|
|
credentials and key material.
|
|
- Structural commands (`ping`, `hello`, `buildInfo`, `listCollections`, `listIndexes`, `collStats`)
|
|
render by name; their arguments are not data-bearing.
|
|
- Everything else renders as `name(...)`: you get the command, never the filter or the document.
|
|
|
|
`isAlwaysRedacted(...)` is the assertion hook so a test can prove no logging path can render an auth
|
|
command's arguments.
|
|
|
|
## 8. Startup validation
|
|
|
|
`MongoStartupValidator` runs at context refresh, before the first request:
|
|
|
|
1. `MongoTopologyProbe` reports the actual `MongoTopology`.
|
|
2. Each declared `MongoTopologyRequirement` is checked against it — a transaction, causal-session or
|
|
change-stream requirement fails closed on `STANDALONE`.
|
|
3. `MongoSecurityProfileValidator` checks credentials and TLS.
|
|
4. `MongoCapabilitySupport` checks declared capabilities against the server version, with
|
|
`MongoSupportLevel` distinguishing `STABLE` / `ADVANCED` / `EXPERIMENTAL` / `UNSUPPORTED`.
|
|
5. `MongoPlatformHealthIndicator` reports the outcome for the readiness probe.
|
|
|
|
A misconfiguration found at startup costs a failed deploy. The same misconfiguration found at runtime
|
|
costs an incident, and the failing operation is rarely the one that reveals the cause.
|
|
|
|
## 9. How the security lane proves any of this
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest --console=plain
|
|
```
|
|
|
|
The lane runs against `MongoAuthenticatedReplicaSetContainer`, which starts mongod with `--auth` and a
|
|
generated keyfile. That detail is the whole lane: Testcontainers' `MongoDBContainer` starts mongod
|
|
*without* `--auth`, so users created on it all have every privilege and a least-privilege assertion
|
|
passes no matter how wrong the roles are. A security test that cannot fail is not a security test.
|
|
|
|
What the lane asserts is the refusal: the `read` role's insert is rejected, and the application role's
|
|
`dropDatabase` is rejected. Then it checks that `MongoSecurityProfileValidator` names the same
|
|
privilege the server just refused.
|