RAG with permissions: where access control has to sit
Timo Wevelsiep•Updated: 01.08.2026Editorial note: Versions, commands and prices may change. Please verify critical steps independently before production use. This guide does not replace individual consulting.
A knowledge system that only shows what the person asking may see? WZ-IT builds RAG systems in which permissions take effect technically before the search - on your own infrastructure, with the architecture laid open. See the internal assistant
Most RAG projects do not fail on answer quality but on one question in the acceptance meeting: "What happens if someone asks about salaries?" Anyone who answers that with a prompt instruction has lost the project at that point - rightly so. This article shows where access control has to sit technically in a RAG system, why the obvious shortcuts do not hold, and how to recognise a design that does. As of August 2026.
Contents
- The prompt is not a security boundary
- Three points where permissions apply
- Pre-filter versus post-filter: what the hit count gives away
- The data model: principals, not usernames
- Resolving groups without slowing the search
- The case everyone forgets: revoked rights
- How to make this auditable
The prompt is not a security boundary
The obvious approach is to put every document into one index and tell the model in the system prompt which content to withhold from whom. That works in the demo and falls over by the third month of operation at the latest.
The reason is structural: once a passage has been retrieved, it is in the model's context. Everything after that is wording. An instruction not to use text that is already present is a request to a system trained to be helpful. It holds for the direct question and breaks on the one after next - "summarise what you have on this topic", "which documents did you just look at", "list the headings".
Then there is the unspectacular case, which in practice is more common than any attack: nobody is trying anything. An employee asks a harmless question, the system pulls a thematically fitting passage from a document that is none of their business, and cites it correctly and helpfully. There is no attacker, only a missing boundary.
Access control therefore belongs where it sits in conventional applications: in the backend, before data retrieval. Not in the presentation layer, and certainly not in a language model.
Three points where permissions apply
In a design that holds, permissions touch the request at three points. Only the middle one decides - the other two make sure it can.
At indexing time. The connector carries the permissions over from the source as it reads the content: owner, direct shares to people, shares to groups, rights inherited from parent folders. These are normalised - "mueller has access" and "group Sales has access" become one uniform list of principals - and stored on every individual passage.
Before the vector search. This is where it is decided. The signed-in user's groups are resolved, a filter is built from them, and the vector database receives the query only together with that filter. What the user may not see is not a lower-ranked hit - it is no hit at all.
At citation time. Before output, the retrieved passages are checked against the source once more. Hours can pass between the last indexing run and the current query, and in that time a share may have been revoked.
Implement only the first point and you hold the permissions in the index while ignoring them at query time. Implement only the second and you have nothing to filter on. The three belong together.
Pre-filter versus post-filter: what the hit count gives away
The most common compromise in existing systems is the post-filter: fetch the twenty most similar passages, then remove the ones the user may not see, and work with the rest. It feels safe because the model genuinely never sees the removed passages. It still has two holes.
The counting leak. If twenty hits are fetched and fourteen removed, the attentive user learns that fourteen further documents exist for their search term that they are not allowed to see. That sounds harmless until you vary the term: "termination Müller" versus "termination Schmidt" - the hit count alone answers the question that was really being asked. In a law firm or an HR department, the mere existence of a matter is the information worth protecting.
The quality leak. When half the best hits drop out, the answer gets noticeably thinner. That, too, is a signal: where the system conspicuously evades, something is there.
A pre-filter has neither problem, because the passages are outside the search space. They cannot be retrieved, counted or inferred. Modern vector databases are built for this: Qdrant applies payload filters during the similarity search rather than sieving afterwards. That is not only safer but also produces better results, because the result list still has the requested length after filtering.
A post-filter is not worthless - as a second line of defence behind the pre-filter it makes sense. As the only line of defence it is a finding.
The data model: principals, not usernames
A passage's permission should not be stored as a username but as a stable principal. The difference becomes relevant at the first marriage or rename: email addresses and login names change, the identity behind them does not.
In practice that means a list of principals in a uniform notation on every passage, treating people and groups alike - an immutable identifier from the identity provider for people, a group identifier for groups. At query time the signed-in user is resolved into exactly that form, and the filter only has to test for overlap.
Two decisions that regularly go wrong here:
Rights belong on the passage, not only on the document. Passages are what gets retrieved. If the permission only hangs on the document, the system has to look up what each hit belongs to - and that is the post-filter you were just avoiding. Duplicating the list on every passage is redundant and correct for exactly that reason: it is where the filter needs it.
Tenants belong in the filter, not in separate collections. A dedicated vector collection per customer or per user sounds clean and becomes an operational problem in the hundreds. A mandatory tenant field in the filter scales better and separates just as sharply, as long as the filter is not optional.
Resolving groups without slowing the search
A user's groups have to come from a trustworthy source - the central identity provider or the directory, resolved server-side. What the client sends is not an ID card. A header a browser can set is a claim.
A directory lookup on every single search would be noticeably slow. Caching for a few minutes is therefore common. That creates a window in which a revoked permission still takes effect - and that window is a deliberate decision that belongs in the documentation and in the conversation with your data protection officer, not a silent convenience.
For cases where the window is too wide there are two routes: deliberate cache invalidation on critical role changes, and the re-authorisation before output covered in the next section.
The case everyone forgets: revoked rights
An employee leaves the project, the share is revoked, and the index still holds it. For how long depends on when the next run is due - with nightly re-indexing, up to 24 hours.
Three things help here, and they belong together:
Changes as events rather than a full sweep. Where the source supports it, share changes are pulled as a feed and applied individually instead of waiting for the nightly run. Nextcloud exposes shares through the OCS Share API; comparable interfaces exist in most document systems.
Re-authorisation before the citation. The second check against the source, immediately before a passage is cited. It costs one call per hit and closes the window completely - and it is the only mechanism that still works when the indexing run has failed.
Deletion that actually deletes. When a document is removed, all its passages have to disappear from the index, not merely be flagged as deleted. A flag that gets forgotten during filtering is worse than no deletion concept, because it suggests safety.
How to make this auditable
A RAG system whose access control has to be taken on trust cannot be signed off in a regulated environment. Four things make it auditable:
Lay the architecture open. Where in the code does the filter sit, which source do the principals come from, does re-authorisation happen? Those are four questions answerable in one paragraph - if the answer gets longer or evasive, that is the result.
Write a two-user test case into the acceptance criteria. The same question, two different logins, different answers - demonstrably different, not merely differently worded. That test belongs in acceptance and in every regression run afterwards.
Log what was filtered. Not for the users but for the audit: how many passages were in the search space, how many after the filter. If those numbers never diverge, the filter may not be doing anything.
Name the cache window. How long do groups count as valid, and what happens on a critical role change? A number in the documentation is auditable; "promptly" is not.
In October 2025 the German data protection authorities published guidance on RAG systems, addressing among other things at which point access rights have to take effect. Building along that line puts you in the easier position in an audit conversation - provided you can show it rather than assert it.
What this means for your project
The permission situation drives the effort of a knowledge project more than the number of documents does. Maintained groups in the directory are the cheap case; individual shares grown over years the expensive one - which is why that check belongs at the start and not in the acceptance meeting.
How RAG works in general is covered in What is RAG?. The same architecture concretely on Nextcloud is in Connect Open WebUI to Nextcloud, and how each vector database implements payload filters is compared in Qdrant vs. pgvector. When the system should not only answer but act, the question shifts - see AI agents: permissions and approvals. Where these requirements bite hardest is shown in Local AI for confidentiality professions.
Rather have it operated?
You'd rather not run Local & Sovereign AI yourself? WZ-IT handles setup, operations and maintenance - GDPR-compliant from Germany.
Frequently Asked Questions
Answers to the most important questions
No. A prompt is an instruction, not access control. Once a passage is in the model's context, it can come back out through a differently phrased question - and even without ill intent, one unlucky retrieval is enough. Access rights have to take effect technically before the search, so impermissible content is never retrieved in the first place.
A pre-filter is a condition of the query: the vector database only searches the region the user is allowed to see. A post-filter removes forbidden hits from a list that has already been fetched - the content was retrieved, and the number of removed hits reveals that something is there. Only the pre-filter holds.
Yes, in two ways. First through the hit count: if ten hits are fetched and six removed, the user learns that four further documents exist for their search term. Second through the quality of the remaining answer - when the best hits are missing, the answer gets noticeably thinner. Both are side channels a pre-filter does not have.
On every chunk. Passages are what gets retrieved, not documents - if the filter only works at document level, the system has to look up which document each hit belongs to, and that is exactly the post-filter you were trying to avoid. Duplicating the ACL in each chunk's payload is redundant, but fast and correct.
From the central identity provider, resolved server-side - never from anything the client sends. A header a browser can set is not an identity. Caching for a few minutes is common because a directory lookup on every search would be needlessly slow; for critical role changes the cache is invalidated deliberately.
Anything can have changed between the last indexing run and the query. That is why a second check belongs before output: the retrieved passages are authorised against the source before they are cited. It costs milliseconds and catches exactly the case that would otherwise be expensive.
By laying the architecture open rather than asserting it. What can be audited: where the filter sits in the code, which principals are resolved from which source, whether re-authorisation happens, and whether there is a test case that asks the same question as two different users and gets different answers. That test case belongs in the acceptance criteria.
More on Local & Sovereign AI
- The open-source LLM stack
- What is LiteLLM?
- What is Langfuse?
- What is vLLM?
- vLLM vs. Ollama
- What is RAG?
- Connect Open WebUI to Nextcloud (RAG with ACLs)
- What is local AI?
- Cloud AI vs. self-hosted
- AI sovereignty for companies
- Which LLM to self-host?
- Sizing GPU & VRAM
- Inference vs. Training
- Qdrant vs. pgvector
- The EU AI Act for companies
- Local AI for confidentiality professions
- Processing documents with AI
- AI agents & automation
- RAG with permissions
- Chatbot or knowledge navigator?
- AI agents: permissions and approvals
- AI assistants and the works council






