Back to the map

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.

DeepMust-knowSenior 14 minPro Updated Nov 2026

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.

User: alicegroups: sales, euQuery + filterindexvisible to alicechunk: sales deckchunk: EU pricingchunk: HR payrollchunk: tenant B2 chunksModelthe filter runs inside the index query, never after top-kred chunks are never scored, so they never reach the context
The user's principals enter the index query. Only chunks whose labels match are scored; the forbidden ones are never candidates, so they cannot leak through rank, count or timing.

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 tenantNamespace per tenantShared index + filter
IsolationPhysical; strongestLogical, enforced by the storeA filter on every query; one missing filter is a leak
CostHighest; small tenants pay for a whole indexModerate; shared hardwareLowest
OpsThousands of indexes to upgrade and back upOne deployment, many namespacesOne index; simplest
Noisy neighboursNoneShared compute, separate dataShared compute and data; big tenants slow everyone
FitsRegulated or very large tenantsMid-size productsMany small tenants, early stage
Permission-aware and multi-tenant retrieval · Backplane