Skip to content
Updated Jun 9, 2026

PII Walls - POC Plan ​

Status: draft · Owner: Chakshu · Companion to:2026-05-26-pii-chinese-walls-design.md, 2026-05-26-pii-at-rest-confidentiality-design.mdDecides: which spikes de-risk the two PII designs before build, and the go/no-go each feeds.


1. Scale anchor ​

members   ~100k        tenants (employers) ~1k        providers ~2k
claims    ~1M          peak reads ~300 req/s          detail-view fraction ~20% ⇒ ~60 detok req/s
budgets:  security-stack added latency  < +50ms p99   on a hot path
          RLS overhead                  < +10%         vs no-RLS, same query
          vault unwrap                  p99 < 10ms     bulk of ≤20 wrapped DEKs / call

Early-growth PMI book. roles-poc's 50k rows is close; bump synthetic data to ~1M so seq-vs-index is visible.


2. Principle ​

One POC per claim whose failure is only visible at scale and would force an architecture change. No POC for claims that are logically true (meet I2), cryptographically standard (AES-GCM, Shamir k-of-n), or governance (separation of duties) - reason about those.


3. The five ​

#Claim under testFailure ⇒ redesignGo/no-go @ scale (§1)EffortStatus
1OPA partial-eval row scope holds for production-shaped policies (multi-hop, disjunctive, large in)disjunctive residual → seq scan; deep join → translator gap ⇒ hand-Go or SpiceDBno-drift over ≥500 actors/role; index/index-only scan on 1M-row claim + joined tables; compile+query p99 < 25ms; residual gaps documented~1wkextends /root/roles-poc (in flight)
2RLS floor survives pgbouncer + per-checkout SET LOCAL GUCtxn-pooling resets/leaks GUCs ⇒ wrong-tenant rows (catastrophic) or forces session pooling; RLS predicate defeats index0 cross-tenant rows over ≥50k randomized checkouts at ~200 concurrent (txn pooling); RLS overhead < +10%; index scans intact~3-4d✅ GO (/root/rls-poc)
3OpenBao Transit sustains detok QPSunwrap latency/throughput bottlenecks Bao ⇒ DEK caching (key exposure) or HSM soonersustained ≥300 unwrap-ops/s (5× the ~60 req/s detail load) at p99 < 10ms on dev-class HW; Bao CPU headroom; bulk-of-20 path tested~3d✅ GO (/root/transit-poc)
4RLS + OPA + vault compose within budgeteach passes alone, sum p99 unacceptableend-to-end added p99 < +50ms on "member opens claim detail" + "employer lists 50 then opens one", sustained @ 300 req/s~1wk✅ GO @ anchor (/root/composed-poc)
5Blind-index β covers the searches the product needsHMAC = exact-equality only; partial name/DOB search needs n-gram/prefix tokens that leak ⇒ UX-vs-confidentiality tradeoff lateenumerate search UIs (member by name/DOB/NHS#, provider, employer lookup); equality blind-index proven; leakage of any prefix scheme quantified; recommendation written~2-3d✅ GO w/ constraint (/root/blindindex-poc)

Status 2026-05-27: all 5 closed. #1 GO (single-table

  • single-hop joins, roles-poc), #2 GO, #3 GO, #4 GO @ anchor, #5 GO w/ constraint. No POC returned a no-go; the binding issues are engineering directives (1-statement RLS context, sidecar OPA, vault-domain placement of partial-match indexes), not architecture changes. Standalone code under /root/{roles-poc,rls-poc,transit-poc,blindindex-poc,composed-poc}, each with FINDINGS.md.

4. Sequence & dependencies ​

#2 RLS+pgbouncer ──┐
#3 Bao Transit   ──┤ (independent, run in parallel)
#1 OPA partial   ──┘ (already in flight)
                    └──► #4 compose latency   (needs 1-3 green)
#5 blind-index search  (standalone, slot in anytime)

Priority order #2 → #3 → #1, but #2 ∥ #3 ∥ #1 are independent and may overlap. #4 gates on the other three. #5 is decoupled.


5. What each POC decides ​

  • #1 → keep OPA partial-eval for row scope, or fall back (hand-Go / SpiceDB) for the shapes it can't translate. (Phase 3 authz.)
  • #2 → GO (/root/rls-poc, FINDINGS.md). RLS floor viable behind txn pooling, conditional on tenant context via SET LOCAL inside the query's transaction - session SET leaked 99.8% of 50k checkouts; safe pattern leaked 0. Predicate is sargable (~0.8ms, same Bitmap Index Scan). Overhead <10% when the GUC is set in one statement (a SECURITY INVOKER function: +6.7-9.7% p50, ~0% p99); the 4-round-trip pattern is +67-110% relative but ~2-7ms absolute. Mandatory: NULLIF(current_setting('app.x',true),'')::int (recycled backends present unset GUC as '' → bare cast errors), FORCE ROW LEVEL SECURITY, app role ≠ table owner, no BYPASSRLS.
  • #3 → GO (/root/transit-poc, FINDINGS.md). OpenBao Transit (2.0.2, same as prod) sustains ~24k single / ~404k batched unwrap-ops/s at p99 <10ms - 80×-1300× the target; ~80MiB RAM, saturates ~9 cores only at 400k/s. No DEK caching needed (good - caching would reintroduce key exposure). Caveat: this is Transit only; the vault's per-detok DB lookup + audit write (~1ms sync) will dominate over the ~3ms unwrap - composed cost is POC #4.
  • #4 → GO @ anchor (/root/composed-poc, FINDINGS.md). Composed "open claim detail" (RLS→OPA→vault lookup→Transit unwrap→audit) p99 9.6ms (c=16) / 29.7ms (c=64) - within +50ms at ~300 req/s. Only at c=200 (~5k req/s, 16× peak) does it break (87.6ms p99), and the cause is shared OPA-over-HTTP (61ms p99), not the data/crypto layers. Directive: sidecar/cache/embed OPA for headroom; audit write is the largest cheap stage (keep sync per I4, batch later if QPS climbs).
  • #5 → GO w/ constraint (/root/blindindex-poc, FINDINGS.md). Blind indexes are sargable, but leakage ∝ 1/cardinality: equality index on a 30-value surname over 100k rows reveals the frequency histogram 1:1; a 1-char prefix token covers ~21k people. So: equality bidx for high-cardinality identifiers (NHS#, email) in the open store; partial-name search index is sensitive-equivalent to the name → keep it in the vault trust domain gated by Φ, or restrict the UI to exact-identifier search. Cheap partial-name search and strong name-confidentiality are mutually exclusive on one store - pick per surface.

6. Not POC'd (reason, don't test) ​

I2 meet · AES-256-GCM strength · Shamir k-of-n mechanics (OpenBao-native) · separation of duties (governance) · I1/I3 as logical identities (they become production canaries, §9b of the access doc, not spikes).

Olly Health Insurance Platform