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>
7.2 KiB
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.
MongoAdminAuthorizationchecks the caller's role against the operation.MongoAdminRuntimeGuardrefuses high-risk operations (highRisk()) unless the runtime profile explicitly permits them; adropCollectionreachable from a running application is a data-loss vector regardless of how well-reviewed the calling code is.MongoAdminAuditRecordrecords 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:
MongoTopologyProbereports the actualMongoTopology.- Each declared
MongoTopologyRequirementis checked against it — a transaction, causal-session or change-stream requirement fails closed onSTANDALONE. MongoSecurityProfileValidatorchecks credentials and TLS.MongoCapabilitySupportchecks declared capabilities against the server version, withMongoSupportLeveldistinguishingSTABLE/ADVANCED/EXPERIMENTAL/UNSUPPORTED.MongoPlatformHealthIndicatorreports 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
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.