Methodology Deliverables Insights Notes Readiness Scope brief

Notes / 04

2026-09-03

Tenant isolation without a bucket per customer

Multi-tenant evidence vaults start with JWT membership checks. That is necessary and not sufficient. Metadata bugs, existence oracles, and a single shared encryption key still expand blast radius. You can harden far before you pay for a bucket per customer.

Thesis: Bind object keys to the tenant id, make missing and unauthorized look identical to clients, and issue a CMK per tenant on a shared bucket (SSE-KMS required on upload). Reserve dedicated buckets for contracts that explicitly buy physical isolation.

1 · Prefix-bound keys

Opaque keys like evidence/obj/{uuid}/file.md trust the database row forever. If a row’s key is rewritten to another tenant’s object, authorization on the row still “succeeds” and you serve the wrong bytes.

Prefer minting keys as evidence/{tenant_id}/{uuid}/{filename} and rejecting any download or presign whose key does not start with that tenant’s prefix. For legacy opaque keys, dual-read only after you have already loaded the row by primary key and confirmed ownership — never from a client-supplied key alone.

2 · Close the existence oracle

Returning 404 when a tenant id does not exist and 403 when it exists but the caller is not a member lets a logged-in user enumerate neighbors. For non-privileged callers, map both cases to the same 404. Keep 403 for a different class of denial (for example “deposit required before vault unlock”) so product semantics stay clear without leaking membership.

Automated tests should assert that cross-tenant list and download attempts are indistinguishable from unknown ids at the HTTP layer.

3 · Per-tenant KMS on a shared bucket

One audit bucket is operationally cheap: lifecycle, Object Lock, logging, and IAM stay centralized. Crypto isolation still helps: create a customer managed key per tenant at engagement start, store the key ARN on the tenant record, and require SSE-KMS (with that key) on PutObject. Compromising one key should not decrypt another tenant’s objects.

Bucket policy can deny insecure transport and deny puts that omit SSE-KMS so clients cannot silently fall back to AES256. Dual-read objects encrypted under a legacy shared key until they are republished — do not block downloads on day one of the migration.

create tenant
  → CreateKey + alias (per tenant)
  → persist key ARN on tenant record
  → PutObject ServerSideEncryption=aws:kms, SSEKMSKeyId=that ARN

close tenant (after retention)
  → schedule key deletion window

4 · Dedicated buckets as an add-on

Some contracts will require a dedicated bucket. Treat that as a flag on create, not the default. Same CMK story; different bucket name; store routing on the tenant record so the evidence store does not guess. Most tenants never need the ops cost.

What this is not

Prefix binding and per-tenant KMS do not replace least-privilege IAM, audit logging, or ops MFA. They shrink the blast radius when application metadata is wrong or one key is exposed. Defense in depth still expects opaque error bodies to clients and public DTOs that omit storage paths and assessor notes.

Checklist

Series start: WAF in front of Function URLs. How we deliver engagements: methodology.

Engineering commentary only — not audit, legal, or certification advice.