PII-at-Rest Confidentiality
Status: draft · Owner: Chakshu · Sibling concern: ABACDecides: how PII is stored so that no single admin can extract it.
This is the confidentiality face of the pii-vault. ABAC is the access face (tokenisation V, surrogates σ, blind index β, detok gated by OPA Φ). This doc sits underneath: how the vault's plaintext system-of-record is stored against privileged insiders - a data-protection problem, not access control.
Block diagram - envelope + trust domains
Sequence - detok with key custody
1. Property & adversaries
Target: no single party can (i) exfiltrate keys, (ii) bulk-extract plaintext, (iii) unilaterally change decryption policy.
Plain-English explanation
The goal in plain terms. No one person - not a database admin, not whoever has root on the server, not any single key-holder - can get the personal data out on their own. Bulk-extracting data, stealing the keys, or quietly changing who's allowed to decrypt each require more than one trusted person to collude, and every legitimate decryption leaves an audit trail. (What this does not stop: someone who compromises a live server can still skim data one record at a time as it flows through - closing that needs hardware enclaves, which we've deferred.)
A_db DB admin psql, reads all tables
A_app service operator deploys, reads service env + logs
A_infra host root memory, disk, process introspection on dev-2
A_kms one OpenBao custodian holds 1 of n Shamir sharescls(f) sensitivity lattice is inherited from the companion doc; this doc protects cls(f) ≥ pii.
2. Crypto structure
per record r, sensitive field set:
DEK_r random 256-bit data key
ct_r = AES-256-GCM(DEK_r, plaintext, nonce_r) nonce random per write
wDEK_r = Transit.wrap(KEK, DEK_r) KEK never leaves OpenBao
stored: { ct_r, wDEK_r, nonce_r, key_version } DEK_r itself never persisted
decrypt: app → Transit.unwrap(wDEK_r) → DEK_r → AES-GCM-open(ct_r)
KEK stays in OpenBao; DEK_r + plaintext transit app memory (the accepted ceiling, §4)OpenBao Transit (OSS - not the Enterprise Transform engine) provides wrap/unwrap with the KEK held inside Bao. OpenBao root/unseal is Shamir k-of-n.
3. Invariants
- C1 (envelope). For
cls(f) ≥ pii: plaintext persists only asAES-256-GCM(DEK_r);DEK_rpersists only asTransit.wrap(KEK, ·); KEK never leaves OpenBao. - C2 (no single admin). unseal, KEK rotation, and Transit policy change each require Shamir k-of-n ⇒ no lone custodian/admin can bring up, rotate, or re-scope decryption.
- C3 (path separation). Automated path = per-record
unwrap, rate-limited (≤ R) + field-scoped- audited. Bulk / export / break-glass = quorum-gated + high-priority audit. No automated bulk-decrypt path exists.
- C4 (separation of duties).
{Shamir custodians} ∩ {app operators} = ∅;{audit readers (compliance)} ∩ {operators} = ∅. One role can neither decrypt-and-hide nor approve-its-own break-glass. - C5 (accepted ceiling). Live-node skimming at rate
≤ Ris detected, not prevented (prevention needs a TEE - declined). Control = rate cap + audit-anomaly alert (§9b of companion doc).
4. Adversary → outcome
| Adversary | Bulk-extract? | Why |
|---|---|---|
A_db (psql) | no | tables hold ct + wDEK; no KEK, no DEK |
| A_app (one operator) | no | automated path rate-limited + audited; bulk path quorum-gated (C3) |
| A_kms (1 share) | no | k-of-n; one share insufficient (C2) |
| A_infra (root, now) | residual | unsealed KEK sits in OpenBao process memory ⇒ root could dump it. Shamir still blocks cold unseal. Closed in Phase B. |
| A_infra (root, Phase B) | no | KEK non-extractable in hardware HSM/KMS |
| any single admin | no (skim only) | can at most skim ≤ R/record on a live node (C5) |
5. Phasing
| Phase | Scope | Closes |
|---|---|---|
| A (now) | envelope via Transit; per-record DEK; verify Shamir k-of-n unseal; C3 path separation (rate-limit + audit automated, quorum break-glass); C4 custody | A_db, A_app, A_kms |
| B | move KEK behind hardware HSM / KMS (Transit backing key in HSM, or migrate) | A_infra residual |
| C | per-subject key hierarchy ⇒ crypto-shredding: erasure = destroy the subject's DEKs, not a row scrub (GDPR Art. 17 as a key-destruction op) | erasure cost + assurance |
6. Determinations (this doc)
- K1 - Envelope encryption with per-record DEKs; KEK in OpenBao Transit (KEK never leaves Bao). Not the Enterprise Transform engine.
- K2 - Shamir k-of-n on unseal + key-admin + policy change. Custodians diverse and disjoint from operators (C4).
- K3 - Two decryption paths only: automated per-record (rate-limited, scoped, audited) and quorum-gated bulk/break-glass. No automated bulk path.
- K4 - Accept C5 ceiling (live-node skim) until/unless a TEE is adopted; mitigate by rate cap + audit anomaly.
- K5 - HSM-backing (Phase B) is the committed path to close the A_infra residual; not deferred indefinitely.
7. Open questions
- DEK granularity: per-record vs per-subject vs per-(subject,field). Per-subject unlocks crypto-shredding (Phase C) but complicates shared-record cases (a claim references member + provider).
- Shamir
(n, k): how many custodians, threshold, and who holds shares - does separation of duties (C4) require ≥1 non-engineering custodian (DPO / counsel / board)? Governance, not code. - Automated-path rate cap
R: legitimate ceiling per service, and the anomaly threshold that pages compliance. - Crypto-agility: key_version is stored, but is rewrap-on-rotate lazy (on next read) or a batch job?
8. Not decided here
- TEE / confidential computing (would close C5) - explicitly out by choice.
- The access face (tokenisation, OPA gating) - lives in the companion doc.
- Snowflake/BI pseudonymisation - separate, deferred.
