Module 08 · Advanced retrieval · Lesson 5 of 6
Permission-aware and multi-tenant retrieval
A user must never retrieve a chunk they could not read at the source: carry access labels through ingestion, filter inside the index, choose a tenancy model, delete on request including cached answers, and attack your own isolation.
Why it matters
A retrieval system is a new read path to every document it indexes, bypassing whatever protected them before. A file share checked group membership on every open; the vector index checks nothing unless you build it. The failure is quiet and total: a user asks "summarise everything about the merger" and the model answers from board papers they were never allowed to see, with citations. Sensitive information disclosure sits second in the OWASP list for LLM applications, and retrieval is the usual route. Interviewers ask because it is the first question a security review asks, and the fix is a set of design choices, not a library.
The idea
The property
State it as an invariant: a user must never retrieve a chunk they could not read at the source. Not "the model should not reveal it"; the model is the last line and a weak one. The chunk must not reach the context at all. Everything else here is machinery for enforcing that sentence at the index.
Labels through ingestion
Permissions travel with the text. At ingestion, every chunk records the source system, the document id, and an access label: a list of allowed groups, or a tenant id. The source of truth is the source system; when sharing changes, the chunks must be relabelled, a change event the Freshness lesson handles. Groups and inheritance are the awkward part: a document inherits its folder's permissions, a user inherits group memberships, and both change. Store the allowed principals on the chunk and expand the user's groups at query time, so the filter is "chunk label intersects user's principals".
Filter inside the index, not after
Two places to apply the filter; one is correct. Post-filtering retrieves the top 50 by similarity, then drops the ones the user may not see. It leaks by construction: the forbidden chunks were scored and ranked, counts and timing reveal them, and if all 50 are forbidden the user gets nothing while a permitted chunk at rank 51 is never seen. Pre-filtering passes the user's principals into the index query so that only permitted chunks are scored at all. Every serious vector store supports a metadata filter evaluated during the search; make it mandatory in code, so a missing filter is a build error.
Tenancy models
Multi-tenant products add a harder boundary: tenants must never see each other at all. One index per tenant is the strongest isolation and the worst operations: thousands of indexes to provision, back up and upgrade, and a tiny tenant pays for a whole index. A namespace per tenant inside one deployment gives logical separation the store enforces, cheaper and still auditable, with the risk that a bug in namespace routing is a cross-tenant leak. A shared index with a mandatory tenant filter is cheapest and easiest to run, and the whole security property rests on one filter being present on every query. Large tenants in a shared index are also noisy neighbours: their traffic and index size slow everyone. Most products start shared and move the largest or most regulated tenants out.
Deletion and the right to be forgotten
When a document is deleted, or a user invokes a right to erasure, the chunks must go, and so must everything derived from them: cached answers, community summaries in a graph, logged prompts that quoted the text, embeddings in any secondary index. Keep a document-to-chunk map so deletion is a lookup, not a scan, and give every cache entry the document ids it depended on. Audit: log which chunks were served to whom, so a leak can be scoped and a regulator answered.
Attacking your own isolation
Permission bugs do not show up in happy-path tests. Write adversarial queries: "list every document", "summarise everything", a document the user should not know exists, another tenant's product names, a user whose group was removed an hour ago. Run them on every deployment, and treat any forbidden chunk in the context, not just in the answer, as a failed test.
| Index per tenant | Namespace per tenant | Shared index + filter | |
|---|---|---|---|
| Isolation | Physical; strongest | Logical, enforced by the store | A filter on every query; one missing filter is a leak |
| Cost | Highest; small tenants pay for a whole index | Moderate; shared hardware | Lowest |
| Ops | Thousands of indexes to upgrade and back up | One deployment, many namespaces | One index; simplest |
| Noisy neighbours | None | Shared compute, separate data | Shared compute and data; big tenants slow everyone |
| Fits | Regulated or very large tenants | Mid-size products | Many small tenants, early stage |