Skip to main content

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-documents disk, DOCUMENTS_VAULT_BUCKET; production sets aws:kms default 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 CryptoShredDocumentHandler branches on storage_encryption and makes the object delete MANDATORY for server_side documents — covering BOTH keys of a confirmed row (the confirmed object AND the sibling upload key it was copied from, derived via CompleteDirectDocumentUploadHandler::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_name scrub, and tombstone apply identically to both.
  • The storage_encryption column (envelope | server_side) is the branch discriminator for every read/verify/shred path; encryption_key_ref is NULL for direct-path rows (made nullable). All reads go through the single DocumentBlobReader so both schemes share identical fail-closed semantics (missing blob / decrypt failure / sha256 mismatch ⇒ 409 DOCUMENT_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.

← Decision log index