GuidesSecure RAG › Permissions

RAG Permissions: Enforcing Entitlements at Query Time

A RAG pipeline enforces permissions correctly when it evaluates the requesting user's entitlements at the moment of the query, against metadata carried by every chunk, as a filter applied before similarity search. Most deployments do none of those three things, and each omission leaks differently.

At a glance
  • Three conditions. Evaluate the requesting user's entitlements at query time, against metadata on every chunk, as a filter applied before similarity search.
  • The index holds the union. It is built by a service account with broad read scope, so it contains what every user can see combined.
  • Post-filtering fails three ways. Ranking degrades, rerankers still process the dropped text, and result counts confirm that withheld documents exist.
  • Freshness is your problem. Nightly re-indexing leaves up to 24 hours in which a revoked user still matches chunks they can no longer open.

Why do index-time permissions fail?

Because the index is built by a connector authenticating as a service account with broad read scope. That is the only practical way to build a complete index on a schedule, so it is what nearly everyone does. The index ends up holding the union of what every user can see.

The permission model was captured once, at build time, for an identity that is not the one making the request.

Nothing about that is wrong on its own. It becomes a leak the moment retrieval serves from that union without re-checking who is asking. The permission model was captured once, at build time, for an identity that is not the one making the request.

PRE-FILTER · ENTITLEMENTS RESOLVED BEFORE SCORING Query arrives with the asker's identity Entitlement filter candidate set narrowed first Similarity search runs over eligible chunks only Top 5 results every one of them eligible Ranking quality is preserved, because the top 5 of the eligible set is what the user asked for. POST-FILTER · RESULTS DISCARDED AFTER SCORING Query arrives identity not yet consulted Similarity search scores the whole index Top 5 results three of them ineligible Discard pass two results survive Two failures at once. The answer is now built from the 4th and 9th best matches, and the missing three are still observable: a shorter result list and a longer response time both signal that something was there.
Pre-filtering and post-filtering are not two implementations of the same control. Post-filtering degrades the answer and still discloses that restricted content matched.

Why is post-filtering not good enough?

Post-filtering retrieves the top-k most similar chunks, then discards the ones the user cannot access. It is the default in most frameworks because it is trivial to bolt on. It fails in three distinct ways.

Ranking degrades. If eight of your top ten results are dropped, the user receives the ninth and tenth best answers and has no idea. Quality complaints follow, and the usual fix is to raise k, which widens the exposure.

Rerankers still see the text. Many stacks rerank before filtering, which means a cross-encoder processed content the user was not entitled to. Whether that constitutes disclosure depends on where the reranker runs and what it logs.

And filtering is observable. Result counts, latency and pagination behaviour can all confirm that a matching document exists even when its content is withheld. Existence is often the sensitive fact, particularly for legal holds, unannounced transactions and personnel matters.

What does a pre-filter look like?

The retriever resolves the requesting identity to a set of permission attributes, then constrains the candidate set to chunks carrying matching attributes before vector similarity is computed. Unentitled chunks are never scored, never reranked, and never counted.

DimensionPre-filterPost-filter
When the entitlement check runsBefore vector similarity is computedAfter the top-k has been selected
Ranking quality preservedYesNo
Unentitled text kept away from the rerankerYesNo
Existence hidden from counts, latency and paginationYesNo
Usual response to quality complaintsNone neededRaise k, which widens the exposure
Effort concentrated inKeeping chunk permission attributes currentBolting the filter on after retrieval

Most production vector stores support metadata filtering that executes as part of the search rather than after it. The engineering work is rarely in the database. It is in making sure every chunk carries current, correct permission attributes, which is where these projects actually stall.

How should permission metadata reach the chunk?

Inherited through chunking, refreshed on change, and expressed as attributes the retriever can evaluate cheaply. When a 90-page document becomes 400 chunks, each chunk needs the access decision that governed the original. Pipelines that store only a source URI leave the retriever nothing to filter on.

Group identifiers usually work better than user lists, because they survive personnel changes without a re-index. The tradeoff is that group membership then has to be resolved at query time against the identity provider, which adds a lookup to the hot path and is worth caching carefully.

Watch for documents that never carried meaningful permissions. Scanned files in a shared folder, exports in object storage, channels that were public by default. Label inheritance gives you nothing there, and those items are frequently the most sensitive in the corpus.

Documents with nothing to inherit

Scanned files in a shared folder, exports in object storage and channels that were public by default give label inheritance nothing to work with, and they are frequently the most sensitive items in the corpus. Default them closed and surface them for review rather than treating unlabeled content as public.

How fast do permissions need to refresh?

As fast as they change in the source system. Nightly re-indexing leaves up to 24 hours during which a revoked user still matches chunks they can no longer open. Offboarding is the case that matters most, because the source system revokes access instantly while the index keeps serving.

What to do

Event-driven updates on ACL changes are the practical minimum. Resolving entitlements live at query time removes the staleness problem entirely, at the cost of a dependency on your identity provider being available and fast.

THE CHAIN AN ENTITLEMENT HAS TO SURVIVE Source ACL the authoritative answer groups, roles, sharing rules Document record usually still intact source URI, owner, path Chunk metadata where it usually breaks text and vector, little else Query-time check needs link 3 to work asker's groups vs chunk ACL Three ways the chain breaks in practice At chunking The chunker copies text and drops everything else. Nobody notices, because retrieval still returns good answers. At re-index Metadata survives the first build and is silently lost on a rebuild months later, when the schema had moved on. At revocation The chain is intact but stale. Access was removed in the source system on Tuesday and the index refreshes on Sunday. An entitlement that does not reach the chunk cannot be enforced at the chunk. Everything after that point is guesswork.
Entitlements have to survive four hops to be enforceable at query time. The third hop is where most pipelines quietly drop them.

How do you verify it works?

Test it rather than reasoning about it. Build a ground-truth entitlement matrix from source-system access control lists, run the same queries as identities at different access levels, and compare what came back against what each was entitled to. The procedure is written up as a RAG leakage self-test.

Re-run after every re-index. Chunk-level permission metadata is the thing that most often fails to survive a rebuild, and a rebuild rarely announces itself as a security event.

Frequently asked questions

Can we use the same ACLs as the source system?

You should, as the source of truth, but they usually need translating. Source systems express permissions as inheritance chains and group hierarchies; a retriever needs flat attributes it can filter on in a single pass. The translation is where errors enter, so test the result rather than the intention.

What about documents with no owner or no permissions?

Default them closed and surface them for review. Treating unlabeled content as public is the most common way genuinely sensitive material ends up in an index, because the files nobody governs are frequently the files nobody should have left lying around.

Does per-user filtering hurt retrieval quality?

A pre-filter does not, because scoring happens across the entitled candidate set and returns the best entitled results. Post-filtering does hurt quality, which is one of the reasons to move the filter earlier rather than compensate by widening k.

Is this the same as row-level security?

It is the same idea applied to a vector index, with one extra failure mode. Row-level security enforces the attributes you wrote; it has no view of whether those attributes still reflect the source system. Freshness is your problem, not the database's.

Sources

Hardshell, What is secure RAG. · Hardshell, RAG leakage self-test.