RAG access control means a retrieval-augmented generation (RAG) feature only retrieves documents the current user could already open in the source system. You get there by copying each document's permissions into the index, applying them inside the search query with an identity your code sets, and keeping them in sync when access changes. Prompt instructions can't do this job, because a model can only withhold what it has already been given.
Why RAG features leak documents
A RAG pipeline has two halves. Ingestion reads documents from a wiki, drive, ticket system or database, splits them into chunks, embeds each chunk and writes it to a vector store. Retrieval embeds the user's question, finds the nearest chunks and passes them to the model.
Leaks start in ingestion. The connector reads the source with a service account that can see everything, and unless someone designs for it, the permissions stay behind. The index becomes a flat pile of chunks from every team, customer and folder.
If the query doesn't narrow that pile to the current user, a junior employee's question can match a chunk from the board minutes, and that chunk can then appear in the answer or a citation. A planted instruction can also ask for it outright, so prompt injection and retrieval permissions belong in the same threat model.
The OWASP entry on vector and embedding weaknesses (LLM08:2025) describes this risk: in a shared vector database, embeddings from one group can be retrieved for another group's queries. Its mitigation is a permission-aware vector store with strict logical partitioning.
Where to enforce RAG access control
The options differ in how reliably they fail closed.
| Approach | How it works | Fits | Watch out for |
|---|---|---|---|
| Filter after retrieval | Search everything, drop chunks the user can't see | Never as the only control | Easy to skip on one code path |
| Metadata filter in the query | Tenant and group IDs on each chunk, filtered during search | Shared indexes with group access | Filter must come from the session every time |
| Namespace per tenant | Each customer's chunks in a separate partition | Multi-tenant SaaS | Still needs group filters inside the tenant |
| Database row-level security | Postgres policies limit which rows a vector search returns | Apps on Postgres with pgvector | Queries must run as the user, not an admin role |
| Recheck at the source | Ask the source's permission API before using each chunk | Highly sensitive collections | Adds latency; use on top of a filter |
For multi-tenant products, combine a tenant partition with group filters inside it. Pinecone's multitenancy guide recommends one namespace per tenant and notes that this reduces the risk of a bug searching the wrong tenant's data. Supabase's RAG with Permissions covers the database route: because pgvector runs inside Postgres, a row-level security (RLS) policy on the chunk table restricts what a similarity search returns.
Copy permissions from the source into the index
Every chunk needs enough metadata to answer "who may see this?" without a trip back to the source:
- the tenant or organization ID
- the source document ID and version
- the groups or roles allowed to read it
- a sensitivity label, if the source has one
Use groups rather than lists of users. A document shared with 400 people should carry one group ID, not 400 user IDs. Pinecone's guide calls filtering by large lists of user IDs an anti-pattern: large filters slow requests, and an $in list over the documented limit makes the request fail.
Some sources restrict a single section, comment or attachment inside a shared document. If your connector can't read those finer permissions, exclude those documents or index only the parts with document-level access.
Write down how each connector maps the source's sharing rules to your groups, and have someone who knows that system review it.
Build the filter in code, never from the model
The identity used for retrieval comes from the authenticated session. Never take it from the prompt, the model's output or a request parameter the user controls.
// Example: tenant and groups come from the session, not from the model
async function retrieve(session: Session, question: string) {
return vectorStore.query({
namespace: session.tenantId,
vector: await embed(question),
topK: 8,
filter: { allowedGroups: { $in: session.groupIds } },
});
}If the model calls retrieval as a tool, give the tool one argument: the search text. Your code adds the tenant and groups. A tool schema that accepts tenantId or filter from the model lets a planted instruction widen the search.
Route every path that returns content through this function: keyword and hybrid search, rerankers, "related documents" panels, autocomplete and answer caches. A cache keyed only on the question can serve one user's answer, built from their documents, to the next user who asks the same thing.
Handle permission changes and deletions
Suppose ingestion copies permissions once a night. A user removed from the finance group at 9 a.m. can keep retrieving finance documents until the next run. Match the window to the data: sync on change events where the source offers them, run a frequent permission-only sync where it doesn't, and recheck at the source for the most sensitive collections.
When a document is deleted at the source, remove its chunks, cached answers and any summaries built from it. When a customer leaves, deleting their namespace is simpler to verify than deleting rows one by one.
Derived data needs the same rules
The OWASP entry lists embedding inversion as a risk: attackers can recover significant source information from embeddings. Treat the vector store as if it held the documents themselves. Restrict direct queries, keep its keys server-side and include it in your retention rules.
The same applies to conversation history that contains retrieved text, summaries generated across many documents, logs of prompts and retrieved chunks, and evaluation datasets built from real questions. A summary of five documents is only safe for users who can read all five. Logs and evaluation sets need access limits that match the most sensitive document they contain.
Common RAG access control mistakes
Each of these lets one user retrieve another user's documents:
- Relying on the system prompt to keep restricted content out of answers.
- One code path, such as an admin search or a background job, that queries the vector store without the filter.
- Permissions copied at first ingestion and never updated.
- Tool schemas that let the model choose the tenant, user or filter.
- Shared caches keyed on the question alone.
- Testing with a single account, so cross-user leaks can't show up.
- Connector service accounts that can read sources the feature was never meant to cover.
How to test RAG access control before launch
Leaks between users are a zero-tolerance failure, so test for them directly:
- Seed canary documents. Put a document with a unique marker string in each tenant and group.
- Query as every role. Ask questions that match each canary, as users who should and shouldn't see it. A marker in the wrong account's answer, citations or logs is a failure.
- Revoke and retry. Remove a test user from a group, wait for your sync window and query again.
- Delete and retry. Delete a canary at the source and confirm it disappears from answers, caches and summaries.
- Attack the filter. Ask the model to search another tenant, pass filter values as tool arguments and hide those requests in an uploaded document.
- Check every entry point. Repeat the queries through the API, widgets, exports and admin tools.
Our guide on how to test an LLM feature before launch shows how to turn these into cases you rerun after every model, prompt or connector change. On Supabase, also review the common row-level security mistakes, since the same policies protect your chunk table.
For an outside check, our LLM and AI agent security testing includes data-leakage tests that try to extract other users' data and retrieved documents, with a retest after fixes. For the wider basics, use the AI app security checklist.
Frequently asked questions
Can the system prompt stop the model from revealing restricted documents?
No. Once a chunk is in the context, the model can be talked into repeating it, and its output varies between runs. Use the prompt to shape tone and refusals, and keep restricted chunks out of the context.
Do we need a separate vector database for each customer?
Usually not. A namespace per tenant gives strong separation for most products. Separate databases make sense when a contract requires them, or when tenants need different regions or encryption keys.
Does permission filtering hurt answer quality?
Filtering inside the query keeps result counts stable. Filtering afterwards can leave the model with two chunks instead of eight. Measure retrieval quality per role, since users with narrow access get weaker answers on topics they can't see.
Can embeddings be stored with fewer controls than the documents?
No. OWASP lists embedding inversion as a way to recover source information from vectors. Restrict embeddings like the text they came from, and delete them when the source is deleted.