Skip to content
Updated Jun 9, 2026

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 shares

cls(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 as AES-256-GCM(DEK_r); DEK_r persists only as Transit.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 ≤ R is detected, not prevented (prevention needs a TEE - declined). Control = rate cap + audit-anomaly alert (§9b of companion doc).

4. Adversary → outcome

AdversaryBulk-extract?Why
A_db (psql)notables hold ct + wDEK; no KEK, no DEK
A_app (one operator)noautomated path rate-limited + audited; bulk path quorum-gated (C3)
A_kms (1 share)nok-of-n; one share insufficient (C2)
A_infra (root, now)residualunsealed KEK sits in OpenBao process memory ⇒ root could dump it. Shamir still blocks cold unseal. Closed in Phase B.
A_infra (root, Phase B)noKEK non-extractable in hardware HSM/KMS
any single adminno (skim only)can at most skim ≤ R/record on a live node (C5)

5. Phasing

PhaseScopeCloses
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 custodyA_db, A_app, A_kms
Bmove KEK behind hardware HSM / KMS (Transit backing key in HSM, or migrate)A_infra residual
Cper-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.

Olly Health Insurance Platform