Phase 4: RAG & Agent Security · 55 min · Python · Vector databases · Row-level security
Retrieval Manipulation, Tenant Isolation & Access Control
A tenant_id column is not tenant isolation until it's enforced inside the search path itself.
Hiring signal: Cross-tenant data leakage in shared RAG infrastructure is one of the most common real findings in AI security audits — engineers who can point to exactly where the filter belongs (pre-ranking, inside search(), not as an optional post-filter) and produce a denied-access audit log are demonstrating the exact skill SaaS companies screen for before letting anyone near multi-tenant retrieval code.
What you will learn
- Explain why filtering by tenant after global ranking is a security bug, not just an inefficiency
- Implement document-level access control (tenant + role) enforced inside the retrieval path itself
- Reproduce a cross-tenant isolation bypass and confirm the fix closes it with an audit log of denied attempts
- Distinguish tenant-level isolation from role-based document permissions within a single tenant
The Problem
Most RAG systems that serve multiple customers from shared infrastructure store every tenant's documents in the same vector index, tagged with a tenant_id field. That's a reasonable cost and operational decision — running a separate vector database per customer doesn't scale. But it means the isolation between Customer A's confidential contract and Customer B's support ticket is enforced entirely in application code, not by the storage layer. If that application-level check has a gap, cosine similarity does not know or care whose document it's ranking.
This is structurally the same bug class as SQL injection or missing row-level security in a traditional multi-tenant database — a query that should be scoped to one tenant's rows instead runs against the whole table. The difference in RAG systems is that the "table scan" is a similarity search, so the bug doesn't look like an obvious SQL statement missing a WHERE tenant_id = ? clause. It looks like a search(query, top_k=5) function that seems to work correctly in every test that happens to only use one tenant's data.
The most common real-world version of this bug: a tenant filter exists somewhere in the codebase — maybe on the primary user-facing search endpoint — but a second code path (an internal debug tool, a new feature, an admin panel, a background job) calls the underlying ranking function directly and nobody re-adds the filter. The vulnerability isn't "we forgot access control exists." It's "access control was bolted onto one entry point instead of built into the retrieval primitive itself."
Where the Filter Belongs
There are two ways to add a tenant filter to a vector search, and only one of them is actually secure:
Filter after ranking (post-filter). Run the similarity search across all documents, get back the global top-k, then discard results that don't belong to the requesting tenant. This is the pattern that produces both a security bug and a correctness bug: if the top-k happens to be dominated by another tenant's high-scoring documents, you can end up with zero or too few results after filtering — and worse, any code path that skips the filter step gets full cross-tenant access with no error, no exception, nothing that looks broken.
Filter before ranking (pre-filter / row-level security). Restrict the candidate set to only documents the requester is authorized to see, and only then compute similarity scores and take the top-k. A document from another tenant is never scored, never ranked, never a candidate — it structurally cannot appear in the output, regardless of how well it would have matched the query. This should be enforced inside the store's search() method itself, taking the requesting identity as a required argument, not as an optional parameter a caller can omit.
Isolation bugs are boring until they're catastrophic
Nobody designs a system intending to leak Customer A's contract to Customer B. It happens because a tenant_id filter that lived in one place (the main API handler) didn't travel with the retrieval function when it got reused elsewhere (an internal tool, a new agent, a batch export job). The fix isn't "remember to filter" — it's making the retrieval function impossible to call without specifying who's asking and enforcing eligibility as part of the function, not as a convention every caller has to remember.
Unlock the full lesson
You've read the first 2 sections. The rest of this lesson covers Role-Based Document Permissions Within a Tenant, Detecting and Auditing Isolation Failures, Build It, What to Practice — plus a hands-on lab, quiz, and project artifact.
Create a free account to unlock Phase 0 and Phase 1 of every course — no credit card.
Browse all courses · View pricing · DeVenture Academy