Files
clean-architecture-backend-…/docs/mongodb/advanced/encryption.md
T
DongHyeonkaandClaude Opus 5 d57d2f62a0 feat(mongodb): implement the MongoDB document persistence platform
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>
2026-08-14 13:41:00 +09:00

92 lines
4.9 KiB
Markdown

# Advanced — CSFLE and Queryable Encryption
**Capabilities:** `MongoCapability.CSFLE`, `MongoCapability.QUERYABLE_ENCRYPTION`
**Properties:** `ca-skeleton.persistence-mongo.advanced.csfle.enabled`,
`ca-skeleton.persistence-mongo.advanced.queryable-encryption.enabled`
**Status:** Advanced.
## Requirements
| | |
|---|---|
| Topology | Replica set or sharded cluster. |
| Server | MongoDB 7.0 or 8.0 (see §4 for the 8.0 query-type limits). |
| Privilege | `MongoPrincipalRole.ENCRYPTION_ADMIN` for the key vault; the application role never holds it. |
| Environment | A real KMS and key vault. A local key provider does not exercise any of the failure modes that matter. |
## 1. CSFLE
`MongoCsfleProfile` binds a collection to its `MongoCsfleFieldPolicy` list, a key vault
`MongoCredentialReference` and the key vault namespace. `MongoCsfleClientFactory` builds the encrypted
client; `MongoDataKeyResolver` resolves data keys.
`MongoCsfleMode`:
| Mode | Queryable | Trade-off |
|---|---|---|
| `RANDOMIZED` | no | Same plaintext encrypts differently each time. The safe default. |
| `DETERMINISTIC` | equality only | Same plaintext always yields the same ciphertext, so equality works — and so does frequency analysis. |
| `UNINDEXED` | no | Stored encrypted, excluded from any index. |
`MongoCsfleFieldPolicy.forPii(field, queryable)` defaults to `RANDOMIZED` when the field is not
queried. Deterministic encryption requires a written `equalityQueryJustification`; the constructor
refuses a blank one, naming frequency analysis. A low-cardinality deterministic field (a status, a
country, a boolean) leaks its distribution to anyone who can read the collection, which is the party
encryption was protecting against.
## 2. Queryable Encryption
`MongoQueryableEncryptionProfile` binds a collection to `MongoEncryptedFieldDescriptor` entries.
`MongoQueryableEncryptionQueryType` has exactly two values:
- `EQUALITY`
- `RANGE` — must declare its domain (`min`, `max`). The constructor refuses a range field without one,
because changing the domain later means re-encrypting the field.
`MongoQueryableEncryptionCollectionManager` owns the collection's lifecycle, because a QE collection
is not just a collection: it carries metadata collections.
## 3. Metadata ownership
`MongoEncryptionMetadataOwnership` maps `customers` to `enxcol_.customers.esc` and
`enxcol_.customers.ecoc`, and reports `__safeContent__`-prefixed indexes as
`MongoMetadataOwnership.ENCRYPTION_MANAGED`.
These are never application-owned and never droppable by drift reconciliation. A drift tool that
drops `enxcol_.customers.ecoc` corrupts the collection's queryability. This is the single most
important integration point between encryption and
[ADR-MONGO-004](../../adr/ADR-MONGO-004-index-schema-admin-plane.md).
## 4. Unsupported combinations
Refused at declaration, not discovered at runtime:
| Combination | Why |
|---|---|
| CSFLE **and** QE on the same collection | Two incompatible encryption schemes over one namespace. Both profile constructors refuse it. |
| CSFLE on a time series collection | `requireNotTimeSeries(true)` raises `MongoOperationRejectedException`. |
| QE `prefix` / `suffix` / `substring` | Not available on the platform's 8.0 baseline. The factory methods throw `UnsupportedOperationException` rather than returning a profile that fails later. |
| Deterministic CSFLE without a justification | `IllegalArgumentException` naming frequency analysis. |
| Range QE without a declared domain | `IllegalArgumentException` naming re-encryption. |
## 5. Failure recovery
| Symptom | Cause | Action |
|---|---|---|
| `MongoEncryptionException` on read | Wrong data key, or the key vault is unreachable | Check KMS reachability and the key vault credential. Data is intact; the client cannot decrypt it. |
| `MongoEncryptionException` on write | KMS permission revoked mid-operation | Restore the grant. Writes fail closed — nothing was written in plaintext. |
| Queries return nothing on a deterministic field | The field was re-keyed | Equality matching is over ciphertext; a new key produces different ciphertext. Re-encrypt the field. |
| QE queries fail after a drift reconciliation | A metadata collection was dropped | Restore from backup. This is why ownership gates drops. |
**Key rotation.** Rotating the customer master key re-wraps the data keys and does not require
re-encrypting documents. Rotating a *data* key does require re-encrypting every document that used
it. These are different operations with different costs, and confusing them is how a rotation becomes
an outage.
## 6. Promotion evidence
Per [ADR-MONGO-ADV-001](../../adr/ADR-MONGO-ADV-001-capability-promotion.md), promotion requires the
real KMS and key vault, plus negative cases that fail closed: wrong key, missing permission, rotation
mid-operation (`MongoAtlasCapabilityContractSuite.kmsFailureModes`). A local key provider certifies
none of these — it never rejects anything.