{"id":69,"date":"2025-12-20T06:41:38","date_gmt":"2025-12-20T06:41:38","guid":{"rendered":"https:\/\/steadyrabbit.in\/blogs\/?p=69"},"modified":"2025-12-20T06:48:02","modified_gmt":"2025-12-20T06:48:02","slug":"keeping-pii-out-of-prompt-land-redaction-secure-retrieval-patterns-for-genai","status":"publish","type":"post","link":"https:\/\/steadyrabbit.in\/blogs\/keeping-pii-out-of-prompt-land-redaction-secure-retrieval-patterns-for-genai\/","title":{"rendered":"Keeping PII Out of Prompt Land\u2014Redaction &amp; Secure Retrieval Patterns for GenAI"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Push a user\u2019s e-mail or SSN into a prompt and you\u2019ve just minted a privacy time-bomb. This deep dive shows how our Micro-GCC squads stop that from happening with a <strong>three-layer defence<\/strong>:<br><br> \u278a open-source PII redaction before vector storage, <br>\u278b AES-GCM\u2013encrypted embeddings, and <br>\u278c placeholder \u201crehydration\u201d at generation time. <br><br>We benchmarked spaCy + Presidio, Amazon Comprehend, and GPT-4o for precision\/recall, then wired the winner into a RAG stack. Copy-paste Docker Compose and Lambda snippets included.<br><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Why Prompts Leak PII\u00a0<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">LLMs store prompts in logs for retraining or debugging. If a user\u2019s phone number lands there, every copy of the model\u2014or your service provider\u2019s logs\u2014now holds regulated data. Regulators don\u2019t care that \u201cit\u2019s just context.\u201d They care that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>GDPR<\/strong> Art. 4(1) defines <em>any<\/em> data identifying a person.<br><\/li>\n\n\n\n<li><strong>CCPA\/DPDP<\/strong> fines apply to <em>sharing<\/em> without consent, including logs.<br><\/li>\n\n\n\n<li>Deleting a prompt from vector DB is easy; deleting it from vendor pretraining buckets is near-impossible.<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Objective:<\/strong> ensure raw PII never crosses the \u201cprompt boundary\u201d; only anonymised tokens do.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Three-Layer Defence Overview\u00a0<\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Ingest Redaction<\/strong> \u2013 Replace PII with deterministic placeholders before embedding.<br><\/li>\n\n\n\n<li><strong>Encrypted Embeddings<\/strong> \u2013 Store vectors in pgvector\/Pinecone with AES-GCM-sealed metadata.<br><\/li>\n\n\n\n<li><strong>Generation Rehydration<\/strong> \u2013 When retrieval returns a placeholder, re-insert the real value <em>after<\/em> the LLM call.<br><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Think of it like a data diode: user data only flows one way\u2014into your secure DB, never into the LLM.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">PII Redaction Engines Benchmarked\u00a0<\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Engine<\/strong><\/td><td><strong>Approach<\/strong><\/td><td><strong>Precision<\/strong><\/td><td><strong>Recall<\/strong><\/td><td><strong>Latency (ms\/chunk)<\/strong><\/td><td><strong>Cost per 1 M words<\/strong><\/td><\/tr><tr><td><strong>spaCy + Presidio<\/strong><\/td><td>NER + regex<\/td><td>0.91<\/td><td><strong>0.93<\/strong><\/td><td><strong>28<\/strong><\/td><td>$0 (self-host)<\/td><\/tr><tr><td>Amazon Comprehend<\/td><td>API NER<\/td><td><strong>0.94<\/strong><\/td><td>0.90<\/td><td>140<\/td><td>$1.00<\/td><\/tr><tr><td>GPT-4o<\/td><td>LLM zero-shot<\/td><td>0.89<\/td><td>0.92<\/td><td>500<\/td><td>$3.40<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Dataset:<\/em> HIPAA-mini + Enron-PII blend, 50 K sentences.<br><strong>Pick:<\/strong><em>spaCy + Presidio<\/em>\u2014best recall under latency &amp; cost budget.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Docker Compose Snippet<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">yaml<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">services:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;redact:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;image: mcr.microsoft.com\/presidio\/analyzer<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;ports: [&#8220;3005:3000&#8221;]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;anonym:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;image: mcr.microsoft.com\/presidio\/anonymizer<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;ports: [&#8220;3006:3000&#8221;]<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Deterministic Placeholder Strategy\u00a0<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">We use the <strong>Iceberg token scheme<\/strong>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ruby<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&lt;EMAIL::sha256(email@example.com&gt;::1&gt;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&lt;PHONE::sha256(+15551234567)::1&gt;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Why deterministic?<\/em><em><br><\/em> \u2013 Same user string \u2192 same token \u2192 better vector clustering.<br>\u2013 Hash acts as salt-ed pseudo ID; attacker can\u2019t reverse without raw.<br>\u2013 Suffix ::1 denotes version for future re-hash migrations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Implementation (Python):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">python<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">from presidio_analyzer import AnalyzerEngine<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">from presidio_anonymizer import AnonymizerEngine<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">analyzer = AnalyzerEngine()<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">anonymizer = AnonymizerEngine()<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def redact(text: str):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;results = analyzer.analyze(text, language=&#8221;en&#8221;)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;redacted = anonymizer.anonymize(text, results,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;operators={&#8220;EMAIL_ADDRESS&#8221;: {&#8220;type&#8221;: &#8220;replace&#8221;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8220;new_value&#8221;: &#8220;&lt;EMAIL::&#8221; + hash_val + &#8220;::1&gt;&#8221;}})<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;return redacted<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Encrypted Embedding Storage\u00a0<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Metadata JSON stored alongside vector:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">json<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">{<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&#8220;token&#8221;: &#8220;&lt;EMAIL::ab12cd::1&gt;&#8221;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&#8220;enc_payload&#8221;: &#8220;Base64:AESGCM(plaintext=email@example.com)&#8221;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&#8220;iv&#8221;: &#8220;random_iv&#8221;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&#8220;aad&#8221;: &#8220;tenant123&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">}<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AES-GCM keys live in KMS; client retrieves plaintext only after user consent.<br><\/li>\n\n\n\n<li>Vector store sees only tokens and ciphertext\u2014useless to attackers.<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform sample for pgvector + KMS IAM role in repo.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Retrieval &amp; Rehydration Flow\u00a0<\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>User query<\/strong> \u2192 embedding model \u2192 vector search (tokens only).<br><\/li>\n\n\n\n<li>Retrieve top-k docs with placeholders.<br><\/li>\n\n\n\n<li><strong>LLM prompt<\/strong> gets redacted docs \u2192 <em>no PII leaks.<\/em><em><br><\/em><\/li>\n\n\n\n<li><strong>Post-LLM<\/strong>: regex replace &lt;EMAIL::hash::1> with decrypted email from KMS.<br><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Latency hit: 7 ms average for decrypt + replace.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">k6 Latency &amp; Cost Benchmark\u00a0<\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Stage<\/strong><\/td><td><strong>Baseline p95<\/strong><\/td><td><strong>With Redaction<\/strong><\/td><td><strong>Delta<\/strong><\/td><\/tr><tr><td>Embed + Search<\/td><td>220 ms<\/td><td>235 ms<\/td><td>+15 ms<\/td><\/tr><tr><td>LLM Generate<\/td><td>420 ms<\/td><td>420 ms<\/td><td>0<\/td><\/tr><tr><td>Rehydrate<\/td><td>N\/A<\/td><td>7 ms<\/td><td>+7 ms<\/td><\/tr><tr><td><strong>Total<\/strong><\/td><td>640 ms<\/td><td><strong>662 ms<\/strong><\/td><td>+22 ms (3.4 %)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Extra $ cost: 0 (self-host), minor KMS calls ($0.05 per 1 M decrypts).<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Case Study \u2014 Emotion-Analysis Wellness App\u00a0<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Problem:<\/em> EU users submit mood journals with e-mails, phone numbers. Prototype leaked PII in OpenAI logs.<br><em>Fix:<\/em> SpaCy\/Presidio redaction + encrypted tokens.<br><em>Outcome:<\/em><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>OpenAI logs 0 PII (verified by ComplyLogs scanner).<br><\/li>\n\n\n\n<li>GDPR DPIA score improved from \u201cMedium\u201d \u2192 \u201cLow.\u201d<br><\/li>\n\n\n\n<li>Latency added 19 ms p95; DAU retention +5 % (users trust privacy banner).<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Pitfalls &amp; Pro Tips\u00a0<\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Pitfall<\/strong><\/td><td><strong>Fix<\/strong><\/td><\/tr><tr><td><strong>False positives (John Major \u2192 PERSON)<\/strong><\/td><td>Add domain dictionaries; raise confidence threshold for names.<\/td><\/tr><tr><td><strong>Hash collision risk<\/strong><\/td><td>Use SHA-256 plus tenant salt in KMS.<\/td><\/tr><tr><td><strong>Placeholder leakage in UI<\/strong><\/td><td>Run Vue\/React sanitiser to display decrypted value only for authorised user.<\/td><\/tr><tr><td><strong>Cost spike in KMS decrypt<\/strong><\/td><td>Batch decrypt per response; cache for 5 min.<\/td><\/tr><tr><td><strong>NER drift with medical jargon<\/strong><\/td><td>Fine-tune spaCy model with 1 K annotated domain sentences (takes &lt; 2 hrs).<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Adoption Roadmap\u00a0<\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Sprint<\/strong><\/td><td><strong>Milestone<\/strong><\/td><\/tr><tr><td>1<\/td><td>Dockerised Presidio, redaction in ingest Lambda<\/td><\/tr><tr><td>2<\/td><td>Implement placeholder scheme &amp; encrypted metadata<\/td><\/tr><tr><td>3<\/td><td>Wrap LLM calls with rehydrate step<\/td><\/tr><tr><td>4<\/td><td>Add k6 latency budget + ComplyLogs PII scanner in CI<\/td><\/tr><tr><td>5<\/td><td>Run DPIA update &amp; update privacy policy copy<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Take-Home Checklist\u00a0<\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Self-host Presidio; benchmark precision\/recall.<br><\/li>\n\n\n\n<li>Replace PII with deterministic hashed tokens.<br><\/li>\n\n\n\n<li>Encrypt raw values in metadata with KMS.<br><\/li>\n\n\n\n<li>Rehydrate after LLM step.<br><\/li>\n\n\n\n<li>Monitor PII scan on logs weekly.<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>Push a user\u2019s e-mail or SSN into a prompt and you\u2019ve just minted a privacy time-bomb. This deep dive shows how our Micro-GCC squads stop that from happening with a three-layer defence: \u278a open-source PII redaction before vector storage, \u278b AES-GCM\u2013encrypted embeddings, and \u278c placeholder \u201crehydration\u201d at generation time. We benchmarked spaCy + Presidio, Amazon [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":20,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-69","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-ml-best-practices"],"_links":{"self":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/69","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/comments?post=69"}],"version-history":[{"count":7,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/69\/revisions"}],"predecessor-version":[{"id":76,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/69\/revisions\/76"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/media\/20"}],"wp:attachment":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/media?parent=69"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/categories?post=69"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/tags?post=69"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}