Retrieval augmented generation collapses the distance between a question and every document behind it. In an enterprise that includes compensation tables, incident reports, and board decks. The failure mode is not a bad answer. It is a correct answer built from a document the caller was never allowed to read.

OWASP put Sensitive Information Disclosure and Vector and Embedding Weaknesses on its Top 10 for LLM applications for exactly this reason. This guide covers where the control belongs, the three ways to build it, and the failure modes I keep finding in reviews.

The control belongs at retrieval

The only durable place to enforce access is the retrieval step. If an unauthorized chunk reaches the prompt, every downstream control is a mitigation rather than a boundary. Attach a security predicate to the query and let the vector store do the filtering.

ts
const results = await qdrant.search('knowledge', {
  vector: embedding,
  limit: 8,
  filter: {
    must: [
      { key: 'tenant_id', match: { value: tenantId } },
      { key: 'acl', match: { any: callerRoles } },
    ],
  },
});

Two properties make this a boundary rather than a filter. The unauthorized chunks never leave the store, so they cannot leak into logs, traces, or caches downstream. And the enforcement point is shared infrastructure, so a new agent or a new UI does not get to reimplement it wrong.

Three enforcement patterns

In practice every permission-aware RAG deployment I have seen is one of three patterns, or a mix.

PatternHow it worksPick it whenWatch out for
Metadata filter in the vector storeEach chunk carries tenant_id and acl payload fields. The query filters before ranking.Most cases. Simple, fast, one system to reason about.Coarse roles only. Complex hierarchies get awkward.
External authorization engineSpiceDB or a similar ReBAC service resolves “can this principal read this document” at query time. Pinecone’s access-control guide walks through this pattern.Permissions mirror Google Drive or SharePoint sharing graphs, with groups nested in groups.Another service in the hot path. Cache decisions per query, not per session.
Source-system passthroughThe index stores the source ACL and the query enforces it. Azure AI Search pulls permission metadata directly from ADLS Gen2 and SharePoint (currently in preview), and Microsoft’s ISE team published a reference architecture for propagating SharePoint permissions downstream in April 2026.Your corpus already lives in one governed system and its ACLs are the truth.Group expansion and token translation get expensive. Cache the caller’s expanded groups.

The wrong choice is usually not between these three. It is building none of them because the demo worked fine without one.

The metadata model decides everything

Whatever pattern you pick, the design question is the same: what security metadata rides along with each chunk. My minimum per chunk:

  • tenant_id, always, even in a single-tenant system today.
  • acl: the roles or group IDs allowed to read the source document.
  • sensitivity: a label like public, internal, restricted, so you can add blanket rules later without re-tagging everything.
  • source_version or a sync timestamp, so you can detect stale permission metadata.

That last field earns its place the first time someone is fired. Permissions at the source change every day, and your index is a snapshot. I call the gap ACL drift, and it is the most common real-world failure: the vector store still serves chunks from a document the caller lost access to last month. Re-sync permission metadata on a schedule and on change events where the source supports them. Updating a payload field is cheap. Re-embedding is not, and permission changes should never trigger it.

Performance: the filter is easy, recall is the problem

Engines such as Qdrant and Milvus index filterable payload fields, so a role filter typically adds single-digit milliseconds. That part is solved.

The harder problem is what filtering does to approximate nearest neighbor search. A June 2026 vision paper on policy-aware vector search formalizes the tension: fine-grained access control, high recall, and low latency pull against each other, because a highly selective filter prunes most of the graph the ANN index wants to traverse. When a caller can see 2% of the corpus, a naive filtered query can return weak results or miss obvious ones. Test with realistic selectivity, not with an admin account that can see everything. If your most restricted roles matter, consider per-tenant partitioning so their search space is dense rather than filtered-sparse.

GraphRAG needs dual-level permissions

Graph retrieval traverses relationships. A node may be visible while the edge that connects it encodes a sensitive relationship. Apply the same security predicate to node lookups and to edge traversals, and audit both.

One more failure mode is specific to GraphRAG and rarely discussed: community summaries. GraphRAG pipelines pre-compute summaries over clusters of entities. A caller may be permitted to see every node in a cluster and still learn something new from the summary, because the summary states the relationship between them. Treat summaries as derived documents with their own ACLs, computed from the most restrictive member, not as free text that inherits nothing.

Where teams get it wrong

PatternFailure modeFix
Post-filter in application codeOver-fetch then discard leaks into logs and tracesFilter inside the query
Prompt-level instructionsModel may ignore or summarize protected textEnforce in retrieval
Per-document ACL onlyGraph edges and summaries bypass document rulesPermission nodes, edges, and summaries
Sync once, never re-syncACL drift serves stale permissions for monthsScheduled plus event-driven permission sync
Test with admin accountsSelective filters starve recall and nobody noticesLoad-test as the most restricted role

Content attacks are a separate axis worth benchmarking too: SafeRAG (ACL 2025) showed that 14 RAG components were broadly vulnerable to noise, context-conflict, soft-ad, and denial-of-service attacks planted in the corpus. Permissions decide who can ask. Corpus hygiene decides what the answer is worth.

Test the boundary, not the demo

Every permission-aware RAG I trust has a small red-team set: ten to twenty questions whose answers live only in restricted documents, run automatically as each role, failing the pipeline if any answer cites a document that role cannot open. Run it in CI and after every re-index. It is the retrieval-layer equivalent of the audit trail I described in AI Agent Audit Log: you cannot claim a boundary you never test.

Further reading