# 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 `` — 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.