ADR-0014 — Envelope encryption at rest via KMS; defer client-side E2E
- Status: Accepted
- Date: 2026-06-11 · Revised: 2026-06-12 (Architecture Freeze V1)
- Related: 01 R11, 02 NG3, 03 NFR-6, ADR-0018
V1 Freeze (2026-06-12): Accepted. Blocker-5 follow-up: the encryption model now has an explicit schema footprint (below) so it is buildable, and V1 uses a per-tenant DEK (not per-object) for simplicity. Secrets-manager wiring (ESO) is ADR-0030 (Deferred); V1 self-host uses a keyfile/ env KMS provider.
Context
BitVault stores sensitive user data and must encrypt it in transit and at rest (NFR-6). The tempting “go big” option is client-side end-to-end (zero-knowledge) encryption — but it breaks server-side search, previews, and dedup, carries a large crypto/key-recovery UX burden, and is a stated non-goal for v1 (NG3). We also need to manage provider credentials and signing keys without leaking them (R11).
Decision
- In transit: TLS everywhere externally; mTLS between services once split (ADR-0001).
- At rest: envelope encryption. A KMS holds the Key-Encryption-Key (KEK);
a per-tenant Data-Encryption-Key (DEK) is generated, used to encrypt that
tenant’s blob bytes (AES-256-GCM), and stored wrapped by the KEK. (Per-object
DEKs are a future option, not V1.) Schema footprint (data model
08): the wrapped DEK + key version live in a
TENANT_KEYrow owned by Identity; eachBLOBrecordsenc_algoand thedek_versionit was sealed under, so rotation re-wraps the DEK without rewriting blobs. Object-store SSE may be layered underneath for defense in depth. - KMS is an abstraction, not a vendor lock: pluggable backends — cloud KMS (AWS/GCP/Azure), HashiCorp Vault, or a dev/self-host keyfile provider. Same interface across deployments (mirrors the storage abstraction, ADR-0005).
- Secrets (DB creds, provider keys, signing keys) come from the environment/ secret manager (K8s Secrets/Vault), never from images or committed config (12-factor, NFR-8). Short-lived where possible.
- Client-side E2E encryption is a deferred future tier (NG3), not the default — recorded here so the trade-off (loses search/preview/dedup) is explicit and a future ADR supersedes this section if pursued.
Consequences
Positive
- Strong at-rest protection with key separation (KEK in KMS, DEKs wrapped) and per-tenant key boundaries that complement tenant isolation (ADR-0007).
- Pluggable KMS keeps self-host (Vault/keyfile) and SaaS (cloud KMS) on one model.
- Server-side features (search, previews, dedup) keep working because the server can decrypt — the deliberate trade vs. E2E (NG3).
Negative / costs
- The server can decrypt data → not zero-knowledge; users trusting the operator is part of the model (honestly stated; E2E tier is the future answer).
- Key lifecycle (rotation, re-wrapping, revocation, and crypto-shred on tenant deletion — destroy the tenant DEK to render all that tenant’s at-rest bytes unrecoverable, satisfying GDPR erasure, NFR-6) is real operational work and must be runbooked. Per-tenant DEKs make per-tenant erasure clean (no shared bytes, ADR-0018).
- Residual existence oracle (accepted for V1): the plaintext
content_hashin a shared table lets a Postgres reader correlate identical files across tenants even though bytes are per-tenant-encrypted. A per-tenant HMAC-keyed blob identity would close it; that hardening is Deferred. - KMS becomes a dependency on the critical path for data access → cache wrapped DEKs carefully; KMS outage handling and least-privilege access required.
Alternatives considered
- Client-side E2E by default (zero-knowledge): rejected for v1 — breaks core features (search/preview/dedup), heavy UX/key-recovery burden; NG3. Kept as a future opt-in tier.
- Rely solely on object-store SSE: insufficient alone — no per-tenant key separation or app-level control; we use it and envelope DEKs.
- No at-rest encryption: rejected — fails NFR-6.