HomeJournalThis post

RAG Access Control Before Vector Search

A deny-first retrieval design that compiles identity into a prefilter, attacks tenant boundaries, and keeps forbidden chunks out of every downstream surface.

JP
JP Casabianca
UI/UX designer and full-stack engineer · Bogotá

RAG access control must run before vector search, not after an answer has already seen forbidden text. The safe unit is a candidate: if the caller cannot read its source document, that chunk never reaches ranking, a prompt, a citation, a trace, or a cache.

This guide follows one principal through policy compilation, retrieval, reranking, and evidence. It uses a worked two-tenant fixture to make the security boundary executable without pretending that one example proves a production identity system.

RAG access control starts with a readable principal

Retrieval cannot enforce a principal it cannot describe. Build a small immutable request object from authenticated claims: subject, tenant, direct groups, delegated roles, policy version, and a request identifier. Reject missing tenant context.

Do not infer it from a document path or from whichever collection returned results. Group expansion should happen through one bounded authority and carry an expiry because memberships change while sessions remain open.

The Azure security-trimming tutorial demonstrates filtering search results with user identities stored on indexed documents. The useful architectural point is earlier than any product-specific query syntax: identity becomes a filter before results are handed to the language model. Call that boundary permission-aware retrieval and log the policy version, never raw private group lists.

For a worked fixture, “Mara” belongs to tenant Acme and group Sales. Acme/Legal, Zephyr/Sales, expired Acme drafts, and public records form separate controls. The fixture is intentionally small enough to reason about by hand. Production RAG access control should preserve the same explicit dimensions even when the identity provider and index expose hundreds of claims.

Permission funnel before similarity searchAn authenticated principal is compiled into tenant and group filters before ANN retrieval. Only allowed candidates continue to reranking and prompting. authenticated claimstenant + groups + policy versionallowed candidate setrank + promptdeny stays out
  • Permission funnel before similarity search
  • Construction logic
  • Interpretive outcome
Figure 1: The funnel narrows authority before relevance is calculated. A high similarity score never widens the caller's readable set.

Compile policy into the candidate query

Put tenant and ACL fields beside every chunk at indexing time, but derive them from the source record rather than accepting uploader-supplied labels. A chunk inherits document identity, owner boundary, allowed groups, sensitivity, lifecycle state, and policy revision. Updates must replace all derived chunks atomically or mark the prior generation unreadable before a new generation appears. Document security trimming is only trustworthy when deletes and permission changes propagate as carefully as creates.

The filter should be conjunctive: correct tenant, readable lifecycle state, and at least one approved access path. Public access is an explicit path, not an absent ACL. Deny entries need a declared precedence rule. If the vector engine cannot express the policy exactly, retrieve from a precomputed allowed partition or move authorization into a store that can; a postfilter over an arbitrary top 20 can both leak and erase recall.

Treat the generated filter as security-sensitive code. Serialize a normalized policy receipt and test it against the source authorization service. RAG access control fails closed when an unsupported operator, truncated group list, stale index schema, or filter-parser error occurs.

“No context available” is a valid product outcome. Guessing from unfiltered neighbors is not.

Attack the boundary with neighbor-shaped documents

Ordinary relevance fixtures rarely expose authorization errors because allowed and forbidden records discuss different things. Create adversarial twins: two tenants use the same project name, two groups own near-identical policy pages, and one superseded document repeats the current title. Give the forbidden twin a higher similarity score. RAG access control passes only when the lower-scoring allowed record is the sole candidate.

Row-security adversarial fixtures provide the database analogue: controls must cross tenants, roles, empty memberships, and revocation. Add malformed ACL arrays, mixed-case group identifiers, Unicode confusables, filter limits, deleted principals, anonymous access, and a user whose group list changes between turns. Assert document identifiers before judging generated prose.

Separate security from retrieval quality. First prove that every returned candidate is readable. Then measure whether enough readable evidence survives for useful answers.

The AWS explanation of retrieval-augmented generation describes the retrieval and generation stages; the worked test inserts an authorization gate between the query and retrieval corpus. That placement makes failure attributable instead of hiding it in an answer-grade score.

CandidateTenant/groupScoreExpected
Acme launch planAcme · Sales0.91Allow
Acme legal draftAcme · Legal0.96Deny
Zephyr launch planZephyr · Sales0.99Deny
Acme expired planAcme · Sales0.94Deny
Figure 2: The matched-topic matrix makes authority, not relevance, determine eligibility. The Zephyr document remains forbidden even when its score is highest.

Keep ranking inside the allowed universe

Apply keyword, vector, and metadata retrieval to the authorized corpus, then fuse and rerank only those candidates. This is the practical meaning of vector search authorization: the authorization predicate constrains both approximate-neighbor traversal and any exact follow-up. A library that accepts a filter but applies it after ANN search needs recall tests because a dense pocket of forbidden neighbors can crowd out the permitted result.

RAG access control and ranking diagnostics should meet at candidate lineage. Record which authorized retriever proposed each document, its rank, and why the final rank changed. Hybrid search RRF shows how lexical and semantic lists can be fused without pretending their raw scores share a scale. Here, each input list must already satisfy the same access predicate.

Rerankers create another exposure surface. Send the minimum allowed text and stable candidate identifiers; do not batch examples from different tenants merely because the model endpoint is shared. A cross-encoder response should be joined against the authorized candidate set, refusing unknown identifiers and duplicates. The final top-k receipt therefore proves two independent facts: every candidate was readable, and relevance ordering happened entirely inside that readable universe.

Runnable artifact: This adversarial tenant prefilter fixture makes the highest-scoring document forbidden, covers empty and malformed principals, and checks that the requested top-k never revives a denied candidate.

Save this worked fixture as rag-access-control.test.mjs and run node rag-access-control.test.mjs. Expected final line: PASS: 10 prefilter assertions.

import assert from "node:assert/strict";
const docs = [
  { id: "a", tenant: "acme", groups: ["sales"], score: .99 },
  { id: "b", tenant: "acme", groups: ["legal"], score: .96 },
  { id: "c", tenant: "zephyr", groups: ["sales"], score: 1.0 },
  { id: "d", tenant: "acme", groups: ["sales", "legal"], score: .72 },
];
function retrieve({ tenant, groups, k = 3 }) {
  if (!tenant || !Array.isArray(groups)) throw new Error("invalid-principal");
  const allowed = docs.filter(d => d.tenant === tenant && d.groups.some(g => groups.includes(g)));
  return allowed.sort((a,b) => b.score-a.score).slice(0,k).map(d => d.id);
}
let n=0; const check=(fn)=>{fn();n++};
check(()=>assert.deepEqual(retrieve({tenant:"acme",groups:["sales"]}),["a","d"]));
check(()=>assert.deepEqual(retrieve({tenant:"acme",groups:["legal"]}),["b","d"]));
check(()=>assert.deepEqual(retrieve({tenant:"zephyr",groups:["sales"]}),["c"]));
check(()=>assert.deepEqual(retrieve({tenant:"acme",groups:[]}),[]));
check(()=>assert.deepEqual(retrieve({tenant:"unknown",groups:["sales"]}),[]));
check(()=>assert.equal(retrieve({tenant:"acme",groups:["sales"]}).includes("c"),false));
check(()=>assert.equal(retrieve({tenant:"acme",groups:["sales"]},).includes("b"),false));
check(()=>assert.throws(()=>retrieve({tenant:"",groups:["sales"]}),/invalid-principal/));
check(()=>assert.throws(()=>retrieve({tenant:"acme",groups:null}),/invalid-principal/));
check(()=>assert.deepEqual(retrieve({tenant:"acme",groups:["sales"],k:1}),["a"]));
assert.equal(n,10); console.log("PASS: 10 prefilter assertions");

Treat prompts, citations, and caches as derived access

A filtered candidate can still leak through an old prompt cache, a shared embedding result, a trace payload, or a citation resolver that fetches the latest document without rechecking authority. Bind every derived object to tenant, policy version, source generation, and the minimum necessary principal scope. Prefer cache misses to a key that cannot prove those dimensions. RAG access control ends only when all copies expire or become unreachable after revocation.

RAG citations that survive document change explains why citations need stable source identity and version evidence. Add authorization at click time because a citation may outlive the membership that created it. The answer can retain a redacted label while the current user receives a clean denial. Never turn a stale citation into a backdoor document fetch.

Trace candidate identifiers, authorization reason codes, counts, and policy hashes. Avoid raw chunks and complete ACLs unless an approved debugging workflow requires them. Prompt-injection content remains untrusted even after authorization; tool-agent injection defenses cover that separate boundary. Read permission answers “may this text enter?” It does not answer “should instructions inside this text execute?”

  1. 1Change policy

    Record the authoritative membership or document ACL revision.

  2. 2Invalidate index

    Remove or replace derived chunks under the same source identity.

  3. 3Expire derivatives

    Purge candidate, prompt, answer, and citation caches with the prior policy key.

  4. 4Re-run controls

    Query matched forbidden twins and preserve the deny receipt.

Figure 3: Revocation is a propagation sequence. The access change is incomplete while any prompt, answer, citation, or cache can still expose the old source.

Make revocation a measured operation

Define a revocation service-level objective from authoritative change to zero readable derivatives. Measure identity cache, ingestion, vector index refresh, result cache, generated-answer cache, and edge propagation separately. RAG access control should expose the slowest stage instead of reporting only that the source database changed. During uncertainty, a policy revision mismatch should deny or force a fresh authorization check.

The ACL 2025 controlled-RAG research is useful evidence that access control and retrieval behavior deserve joint evaluation. A deployment receipt should still distinguish the paper's setting from the actual identity, index, and cache stack. Record corpus snapshot, principal fixtures, filter representation, engine version, approximate-search settings, and every denial reason.

Operational dashboards need negative signals: forbidden-candidate count before prompt assembly must remain zero; unknown candidate joins must remain zero; ACL compilation errors must fail closed; and revocation latency must stay within its budget. Sampled manual review can inspect receipts without revealing content. A security alert should name the violated boundary and request identifier, not dump the chunk that caused it into a broadly readable channel.

Publish RAG ACL filters as evidence, not decoration

The production checklist is compact. Authenticate the caller; normalize tenant and groups; compile policy; authorize at candidate generation; rerank only permitted records; recheck citation fetches; partition every derivative cache; and exercise revocation. Store those filter receipts in a redacted, canonical form so a reviewer can compare intent with the vector engine's actual query.

RAG access control also needs an explicit abstention message. When zero evidence survives, say that no readable sources support an answer and offer a path to request access. Do not reveal forbidden titles, counts, owners, or snippets. When partial evidence survives, citations must identify only readable sources and the answer should avoid guessing about missing material.

Ship the worked fixture beside production policy adapters and rerun it when claim mappings, index schemas, search libraries, rerankers, or cache keys change. The durable proof is not a screenshot of an allowed answer. It is a set of matched forbidden neighbors that remain absent at every stage, plus a revocation clock showing how quickly their derivatives disappear. That is the point at which authorization has become part of retrieval architecture rather than an optimistic cleanup step.