Metadata filtering and access control in RAG

GuideLast reviewed Oct 16, 2026By Himanshu Ramchandani
Short answer

Every chunk in your index should carry metadata: where it came from, its page and section, its date and version, what it's about, and who is allowed to see it. Metadata filters narrow retrieval to the right scope (this product, this year, this region), and permission filters make sure a user only ever retrieves chunks from documents they could open themselves. Enforce permissions in the retrieval query, from the authenticated user's identity, never by asking the model to hide things, because anything that reaches the prompt can reach the answer.

Why metadata matters

Without metadata, every chunk in your index is equal. A question about the 2026 travel policy can retrieve the 2023 version, because the text is nearly the same. A question about the X200 laptop can retrieve chunks about the X300. A question from an intern can retrieve the salary bands document from HR.

Semantic similarity can't tell these apart, because they are similar. Metadata can.

What metadata to store

Field Used for
document_id, chunk_id Updates, deletions, citations
source, title, url Citations and links
page, section, heading path Citations, context, structure-aware retrieval
created_at, updated_at, version Freshness, removing outdated versions
doc_type (policy, manual, FAQ, contract) Filtering and routing
product, region, department, language Scoping retrieval
tenant_id Separating customers in a shared system
allowed_groups or allowed_users Access control
sensitivity (public, internal, confidential) Access control and compliance

Capture these during parsing and ingestion, from the source system wherever possible, because adding metadata later means re-processing everything.

Filtering for relevance

Use metadata filters when the question, the user or the application tells you the scope.

  • The user is in the UK office, so filter to region UK or global.
  • The question mentions 2026, so filter to documents valid in 2026.
  • The user is in the support app for the X200, so filter to product X200.
  • Only the latest version of each policy is active, so filter to is_latest = true.

Filters can come from the user's profile, from the application context, or from the question itself through query rewriting that extracts them. Filters that are too strict return nothing, so fall back to a broader search, and tell the model when results are limited.

Access control: the rule

A RAG system must never show a user information from a document they aren't allowed to open.

That sounds obvious, and it is the most common serious failure in enterprise RAG, because during ingestion all documents end up in one index, and the original permissions from SharePoint, Google Drive or the document system are lost. Then anyone can ask the assistant about salaries, legal disputes or a confidential acquisition, and get an answer.

How to enforce it

  1. Capture permissions at ingestion. Read who can access each document from the source system, and store it on every chunk, for example as a list of allowed groups.
  2. Identify the user. Every query comes from an authenticated user, and your backend knows their user ID and groups.
  3. Filter in the retrieval query. Add a filter so only chunks the user's groups can access are searched. Do this in the vector database query itself, not after retrieval.
  4. Never rely on the prompt. Don't retrieve everything and tell the model "don't reveal confidential information". Once text is in the prompt, it can appear in the answer, and prompt injection can make that happen on purpose.
  5. Keep permissions in sync. When access changes in the source system, update the chunks, and when a document is deleted, delete its chunks.
  6. Log what was retrieved for whom, for audits.
def allowed(chunk_meta: dict, user_groups: set[str]) -> bool:
    return bool(user_groups & set(chunk_meta.get("allowed_groups", [])))


chunks = [
    {"text": "Leave policy: 24 days paid leave per year.", "meta": {"allowed_groups": ["all-employees"]}},
    {"text": "Salary bands for 2026, grade L4 to L7.", "meta": {"allowed_groups": ["hr", "leadership"]}},
]

intern_groups = {"all-employees", "interns"}
visible = [c["text"] for c in chunks if allowed(c["meta"], intern_groups)]
print(visible)

In a real system, this check is a filter in the vector database query, like a where clause on allowed_groups, so restricted chunks are never even scored.

Multi-tenant systems

If one system serves several customers, each customer's data must be isolated. The options are a separate index or collection per tenant, which gives the strongest isolation and is simple to reason about, or one shared index with a mandatory tenant_id filter on every query, which is more efficient and needs very careful enforcement and testing. For sensitive customers, separate collections are often worth the extra cost. See multi-tenant AI platforms.

Sensitive data inside documents

Sometimes a user may see a document, but parts of it shouldn't go to the model or the logs, like personal data, account numbers or health information. Options include masking those values before indexing or before sending them to the model, and restoring them only in the final interface for users allowed to see them. See RAG for regulated data.

Test it

Add access tests to your evaluation:

  • Ask questions as users without access to a document, and check that nothing from it is retrieved or answered.
  • Try prompt injection that asks for confidential content.
  • Remove a user from a group and check that their access disappears.
  • Check that deleted documents can no longer be retrieved.

From the floor

When we built the HR query bot in the cohort, the data was the HR department's, which is sensitive by nature, so before the RAG part we added authentication for the first time in our projects: OAuth2, hashed passwords and access tokens that expire after 30 minutes. We also discussed whether sensitive HR data should push the system to on-premises, and the honest answer for that project was that there wasn't a budget for it, so we went with the cloud and designed the security and authentication carefully, keeping the option to move later.

For Neural Vault, one of the privacy features was masking sensitive data before processing and restoring it only in the final interface, so the model works with masked values and the user still sees the real ones.

Sources