You enforce document permissions in retrieval-augmented generation (RAG) by filtering on the caller's entitlements at query time, inside the vector store query, before any chunk is ranked or handed to the model. Each chunk carries the access metadata of the document it came from. The retriever resolves who is asking — a human, or an agent acting for a human — and only searches chunks that principal may read. Everything else, such as prompt instructions that tell the model to ignore restricted content, is decoration. Then you log which chunks reached whom, so the decision can be reviewed later.
That is the short answer. The rest of this post covers where teams get it wrong: the ingestion step that drops ACLs, the agent that retrieves with its own service account, and the sync lag that turns a revoked share into a leak.
Why Similarity Search Ignores Permissions
A vector index answers one question: which stored vectors are closest to this query vector? It has no notion of who is asking. If a confidential board memo and a public FAQ are both in the index, a query about "Q3 headcount plan" will rank the memo highly because it is relevant, not because the asker may read it.
The source systems — SharePoint, Google Drive, Confluence, a ticketing tool — all had permissions. The typical ingestion pipeline flattens them away: it reads documents with a privileged connector, chunks them, embeds them, and writes vectors plus some text into a shared collection. Access control that lived in the source is now gone.
OWASP lists this failure under LLM08:2025 Vector and Embedding Weaknesses, which names unauthorized access and cross-context information leaks as core risks and recommends permission-aware vector stores with fine-grained access controls. We covered the poisoning half of that entry in RAG pipeline security; this post is the access-control half.
Three Ways to Enforce Access in Retrieval
| Approach | How it works | Strength | Weakness |
|---|---|---|---|
| Separate index per tenant or per group | Each permission boundary gets its own collection; the retriever picks the collection from the caller's identity | Hard isolation; a bug in filtering cannot cross the boundary | Explodes when permissions are per-document; shared documents get duplicated |
| Metadata pre-filter | Each chunk stores ACL fields; the query includes a filter such as tenant = X AND groups ∩ caller_groups ≠ ∅ evaluated before similarity ranking |
Scales to per-document ACLs; one index | Depends on correct, current metadata and on the store's filter performance |
| Post-filter after retrieval | Retrieve top-k, then check each result against the source system's permission API | Uses the live source of truth | Top-k may be all forbidden, so results go empty; easy to forget the check on one code path |
Most production systems combine them: a hard per-tenant boundary (separate collection or a mandatory tenant filter), a metadata pre-filter for groups and classification inside the tenant, and an optional post-check against the source system for high-sensitivity collections.
The rule that matters: the filter is a hard predicate in the query, not a ranking preference. A "boost" for permitted documents still lets forbidden ones through when nothing permitted matches.
Carry the ACL Through Ingestion
Pre-filtering only works if every chunk knows its permissions. At ingestion, copy the following from the source document to every chunk:
- Tenant or organization ID — mandatory, never inferred later.
- Allowed principals — user IDs and group IDs that may read, as resolved by the source system.
- Classification label — public, internal, confidential, restricted, or your own scheme.
- Source document ID and version — so a permission change on the source can find every chunk derived from it.
Two traps. First, group expansion: if you store group names, the retriever must resolve the caller's group memberships at query time from the identity provider, not from a cached copy weeks old. Second, derived content: summaries, extracted tables and "knowledge cards" built from several documents must inherit the most restrictive ACL of their inputs, or they become a laundering path around document permissions.
Agents Retrieve on Someone's Behalf
Human-facing RAG chat has an obvious caller: the logged-in user. Agents blur this. An agent that triages tickets, drafts replies or answers questions in Slack often runs under its own service identity, and that identity is usually granted broad read access so it "can find things." If the retriever filters on the agent's identity, every user who can talk to the agent can read everything the agent can read. That is the confused deputy problem applied to retrieval.
The fix is to filter on the effective principal:
- Delegated retrieval — the agent acts for a specific user. Filter on the intersection: chunks the user may read and the agent is allowed to access. The user cannot gain reach through the agent, and the agent cannot exceed its own scope.
- Autonomous retrieval — no user in the loop, for example a nightly report. Filter on the agent's own entitlements, and keep those narrow. Treat the agent as a non-human identity with its own least-privilege grant, as described in how to implement least privilege for agents.
Pass the user identity to the retriever as a verified claim (a signed token or a server-side session lookup), not as a string the model fills into a tool argument. A model that can write user_id into a search call can be prompted into writing someone else's.
Role- versus attribute-based models both work here. Roles cover coarse collections; attributes cover per-document rules such as "same department and classification ≤ internal." The tradeoffs are in RBAC vs ABAC for AI platforms.
Keep Permissions in Sync
The index is a copy, and copies go stale. When someone removes a colleague from a shared folder, the chunks from that folder still list the old principal until the pipeline catches up. During that window, retrieval serves revoked content.
Close the window:
- Subscribe to permission-change events from the source system where available, and update chunk metadata without re-embedding.
- Run a periodic reconciliation that compares chunk ACLs against the source for every document version.
- On document deletion, delete every derived chunk and summary, not just the original vectors.
- For restricted collections, add a post-retrieval check against the live source API so a stale index cannot be the only gate.
Log What Was Served to Whom
Access control without a record is unverifiable. For every retrieval, record the effective principal (user, agent, or both), the task or session, the query, the collection, the filter applied, and the IDs of the chunks returned. Store chunk IDs, not chunk text, unless policy requires the content; this keeps the log from becoming a second copy of the sensitive corpus.
This record answers the questions that come after an incident or during an access review: did anyone outside finance retrieve this memo, which agent run saw this contract, was the revoked share used after revocation. The same pattern appears in governing LlamaIndex agents' data access, where the retriever is the enforcement point.
Permission-Aware RAG Checklist
How Praesidia Approaches Retrieval Access
When retrieval is an MCP tool call, Praesidia governs it like any other tool call. The agent reaches the retrieval server's tools through Praesidia only over an active connection to that server. In enforce mode, its tool policy refuses any tool that none of its rules allows, and the connection's per-tool permissions can deny a tool or constrain its argument values, such as the collections a search may query.
Praesidia does not enforce document ACLs or tenant boundaries inside the vector store; that remains the store's own query filter, as described above. What it adds is the governance layer around it: retrievals made as tool calls through Praesidia are recorded with the agent, the task, the arguments and, where returned, the result, subject to redaction. That gives you a "who retrieved what" record for retrievals routed through it; calls that bypass Praesidia are not recorded. Guardrails can also check retrieved content for PII before the model reads it, with block, redact, or warn dispositions. For the broader model, see the AI agent security guide.