D87 — Documents vault encryption at rest for the direct path: bucket-default SSE-KMS replaces app envelope encryption (RESOLVED, extends D43/D78, trade-off owned)
Architecture decision record, relocated verbatim from the retired single-file
docs/tenancy/decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.
D43's envelope encryption (per-file AES-256-GCM DEK, app-wrapped) REQUIRES the app to touch the plaintext at write time. The D86 direct path exists precisely so the app never touches the client's upload — so envelope encryption CANNOT apply to direct-path blobs. This is a real property trade, owned explicitly rather than silently dropped:
- Decision: bucket-default SSE-KMS is the at-rest floor for the direct bucket
(
s3-documentsdisk,DOCUMENTS_VAULT_BUCKET; production setsaws:kmsdefault encryption with a dedicated CMK as deploy config). Per-request SSE headers on the presigned PUT were evaluated and rejected: the SDK only signs SSE-KMS headers into the presigned request if the client then actually SENDS them (a cooperating client requirement — an omitted header stores the object however the bucket default says), so client cooperation is the only thing per-request headers add, and it is unreliable by definition. A bucket default needs no client behaviour at all — the same reasoning as D85's bucket-default Object-Lock. MinIO local emulation is honest-by-note: MinIO supports SSE-KMS only via a KES sidecar, so local direct-path objects are NOT KMS-encrypted — an accepted dev/prod divergence, documented on the disk config. - What each path now guarantees. Proxied path (unchanged): app envelope encryption, per-file
DEK, GCM tamper-evidence, DEK-drop crypto-shred. Direct path: SSE-KMS at rest (bucket-wide key,
not per-file), integrity via the confirm-time sha256 — computed over the D86 confirmed copy
(the only key ever served; the copy closes the presigned-PUT swap window, so the hash anchors
bytes no client-writable URL can reach) and re-verified on read/sweep (no GCM layer — tamper
detection is the hash, not an auth tag), and RTBF erasure = the OBJECT DELETE: with no
per-file DEK there is nothing to crypto-shred, so
CryptoShredDocumentHandlerbranches onstorage_encryptionand makes the object delete MANDATORY forserver_sidedocuments — covering BOTH keys of a confirmed row (the confirmed object AND the sibling upload key it was copied from, derived viaCompleteDirectDocumentUploadHandler::siblingUploadPath()— the post-confirm retirement of the upload key is best-effort, so a surviving orphan carries the same subject bytes and must not outlive the tombstone; a never-confirmed row whose path IS the upload key erases just that one object), each delete performed and VERIFIED (!exists) BEFORE the tombstone/event/audit claim stands (a failed delete on either key fails the RTBF command loudly- retryably; the accepted residual is the safe direction — erased-without-claim on a post-delete
commit failure, never claimed-without-erasure). For
envelope documents the blob delete stays optional behind
documents.rtbf.delete_blob(after-commit, checked + logged — the DEK drop already erases). The D78 retention guard,original_namescrub, and tombstone apply identically to both.
- retryably; the accepted residual is the safe direction — erased-without-claim on a post-delete
commit failure, never claimed-without-erasure). For
envelope documents the blob delete stays optional behind
- The
storage_encryptioncolumn (envelope|server_side) is the branch discriminator for every read/verify/shred path;encryption_key_refis NULL for direct-path rows (made nullable). All reads go through the singleDocumentBlobReaderso both schemes share identical fail-closed semantics (missing blob / decrypt failure / sha256 mismatch ⇒ 409DOCUMENT_INTEGRITY_FAILURE). - Residual risk accepted: direct-path blobs lose per-file crypto-shred granularity and GCM tamper-evidence; the compensations are the private bucket + presigned-only access + per-serve logging (D86), SSE-KMS custody of the key material, sha256 integrity, and delete-based erasure. Tenants needing the envelope property keep the proxied path (the direct flow is opt-in, per-deployment). KMS key-material destruction policy for the vault CMK remains a D78 prerequisite to enabling real erasure.