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.)
  • #2GO (/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.
  • #3GO (/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.
  • #4GO @ 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).
  • #5GO 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