RAG Access Control: The IAM Layer Your Vector Database Doesn’t Have

Reading Time: 8 minutes

The Non-Human Identity Problem Is BackRAG Access ControlOIDC and Workload Identity for LLM Pipelines


TL;DR

  • Most vector databases have no document-level access control by default — if a document was indexed, any query can retrieve it
  • In a multi-user RAG application, this means User A’s confidential documents can end up in User B’s context window without any API call, auth token, or permission check failing
  • RAG access control requires enforcement at three separate layers: at ingestion (what gets indexed), at retrieval (what the query can return), and at the application layer (what the model receives)
  • The technical solutions exist — namespace isolation, metadata filtering, Row Level Security on pgvector, Weaviate RBAC — but they require deliberate implementation; they are not defaults
  • The IAM principle is the same one that solved the S3 bucket problem: you must assume all data in the store is sensitive, and access must be granted explicitly, not assumed by adjacency

OWASP Mapping: OWASP LLM08 — Vector and Embedding Weaknesses. This episode covers the access control gap that makes vector databases the most commonly misconfigured IAM boundary in LLM deployments.


The Big Picture

RAG PIPELINE: WHERE ACCESS CONTROL BREAKS DOWN

User A                     User B
  │                           │
  ▼                           ▼
[Query: "summarize           [Query: "what are our
 my performance review"]      Q4 revenue projections?"]
         │                           │
         └──────────┬────────────────┘
                    ▼
            ┌──────────────┐
            │  LLM / RAG   │
            │  Application │
            └──────┬───────┘
                   │
                   ▼ similarity search
            ┌──────────────────────────────┐
            │     Vector Database          │
            │  ┌─────────────────────────┐ │
            │  │ performance_review_a    │ │ ← User A's private doc
            │  │ q4_revenue_projections  │ │ ← Finance-only doc
            │  │ engineering_runbook     │ │ ← Internal ops doc
            │  │ hr_salary_bands         │ │ ← HR-only doc
            │  │ customer_contracts      │ │ ← Legal-only doc
            │  └─────────────────────────┘ │
            │  ← ONE collection, no ACLs   │
            └──────────────────────────────┘

Without access control, User B's query about "projections"
can semantically retrieve User A's performance review,
the salary band document, and customer contracts
— all in a single unauthenticated vector similarity search.

RAG access control is the IAM problem that most vector database deployments skip entirely. The retrieval layer is effectively a permission-free zone: if a document is indexed, it is queryable. The permissions model that governs who uploaded the document has no connection to the permissions model that governs who can retrieve it.


Why This Happens

The fastest path to a working RAG system is also the path with no access control:

  1. Index all your documents into one vector store collection
  2. At query time, run a similarity search
  3. Pass the top-N results to the model as context

This works. It produces a demo that impresses stakeholders. And it has no concept of “is the user who submitted this query authorized to read these retrieved documents?”

The problem is structural: vector similarity search is a mathematical operation on embeddings. It finds nearest neighbors in a high-dimensional space. It does not have a concept of authorization. The database returns the most semantically similar documents to the query — full stop. It does not know or care who is asking or what they are allowed to see.

This is the same failure class as public S3 buckets. The storage system itself is not wrong — it returned what it was asked for. The mistake is not building the access control layer that determines what can be asked.

The consequence in RAG is worse than in S3 in one specific way: the exposure is invisible. When someone accesses a public S3 bucket, there’s an explicit HTTP request and a 200 response in the access logs. In RAG, the unauthorized document surfaces inside a model response. There’s no explicit “unauthorized document retrieved” event. The application sent a query; the database returned results; the model included them in its answer. Everything “worked.”


How User A’s Data Ends Up in User B’s Context

Three realistic scenarios:

Scenario 1: Semantic proximity

User A uploads a performance review: “Alice achieved 94% of her targets in Q3, and her compensation adjustment is scheduled for December.”

User B asks about Q3 performance metrics for the engineering team.

The similarity search returns User A’s document as a top-N result because it contains “Q3,” “performance,” and numerical metrics. The model includes it in the context and may summarize or reference it in its answer.

No authentication was bypassed. No API was misused. A semantically similar document was retrieved by a semantically similar query.

Scenario 2: Shared namespace, different sensitivity levels

A knowledge base contains both public documentation (product manuals, FAQ articles) and internal documents (salary bands, acquisition targets, unreleased roadmap). They’re all indexed together because the indexing pipeline processes all documents from a shared document store.

A user with access to the public KB submits queries that — through careful phrasing — retrieve internal documents via semantic overlap. They never access the internal document store directly. They access it through the model’s context window.

Scenario 3: Cross-tenant retrieval

A SaaS application uses a shared vector database for all customers. Customer A uploads their proprietary process documentation. Customer B’s query, framed in similar terminology, retrieves Customer A’s documents.

This is a data breach. It does not involve any failed authentication — it involves missing authorization at the retrieval layer.


The Three Enforcement Points

Fixing RAG access control requires thinking about authorization at three distinct layers, not one.

Layer 1: Ingestion — What Gets Indexed

Every document that enters the vector store should be tagged with the identity of its owner and the scope of who is authorized to retrieve it. This metadata travels with the document through the pipeline.

vector_store.upsert(
    id="doc_performance_review_alice_2024",
    vector=embedding,
    metadata={
        "owner_user_id": "user_alice",
        "authorized_roles": ["hr_manager", "alice"],
        "sensitivity": "restricted",
        "department": "engineering",
    }
)

If the document has no access control metadata, treat it as the most sensitive class, not the least. Default-deny.

This requires the indexing pipeline to have access to the permission model. The pipeline needs to know, at index time, who can retrieve this document. That means the indexing service must be integrated with your IAM system — not just your document store.

Layer 2: Retrieval — What the Query Can Return

Every similarity search should be filtered by the requesting user’s authorization context. Most vector databases support metadata filtering at query time.

# Retrieve only documents the requesting user is authorized to see
results = vector_store.query(
    vector=query_embedding,
    filter={
        "$or": [
            {"owner_user_id": {"$eq": current_user_id}},
            {"authorized_roles": {"$in": current_user_roles}},
        ]
    },
    top_k=5
)

This is the equivalent of parameterized queries in SQL — you are not filtering after the fact, you are scoping the search space before retrieval. Only documents the user is authorized to see are candidates for the similarity search.

What each vector store supports:

Database Access Control Mechanism Granularity
Pinecone Namespaces (partition isolation) Namespace-level
Weaviate RBAC (per-class and per-object) Object-level
pgvector PostgreSQL Row Level Security (RLS) Row-level
Qdrant Payload filters at query time Per-document metadata
Chroma Collections with custom metadata filters Collection + filter
Milvus Partition keys + role-based access Partition-level

pgvector via PostgreSQL RLS is the strongest option — authorization is enforced at the database engine level, not in application code. The query cannot return rows the RLS policy does not permit, regardless of how the application constructs the query.

-- PostgreSQL RLS policy for vector store table
ALTER TABLE document_embeddings ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_isolation ON document_embeddings
    USING (
        owner_user_id = current_setting('app.current_user_id')
        OR current_setting('app.current_user_id') = ANY(authorized_user_ids)
    );

With this policy, even if the application layer is compromised or misconfigured, the database will not return unauthorized rows.

Layer 3: Application — What the Model Receives

Even with ingestion-time tagging and retrieval-time filtering, there is a third layer: validating retrieved documents before they are passed to the model.

This is the paranoid layer. It assumes retrieval filtering may have gaps (a new document type that wasn’t tagged, a filter logic bug, a configuration drift). Before the retrieved chunks enter the model’s context window, verify their authorization against your canonical permission system.

# Post-retrieval authorization check
authorized_chunks = [
    chunk for chunk in retrieved_chunks
    if permissions.is_authorized(
        user_id=current_user_id,
        resource_id=chunk.metadata["document_id"],
        action="read"
    )
]
# Only pass authorized_chunks to the model

This is defense-in-depth for the retrieval layer. Each layer can catch failures in the layer before it.


The Service Account Problem in RAG Pipelines

Beyond user-level access control, RAG pipelines have a service account problem.

A typical RAG pipeline has three services: an embedding service (converts documents to vectors), a retrieval service (queries the vector store), and a generation service (calls the LLM with the retrieved context). In most deployments, all three run under the same service account with broad access to the vector store.

This creates a privilege escalation path: if an attacker can compromise the generation service (via prompt injection, for example), they can pivot to the retrieval service’s permissions because they’re the same identity. The generation service doesn’t need write access to the vector store — but if it runs under the same account as the embedding service, it has it.

Correct architecture:

Embedding Service   ── service-account: embed-sa
  └─ Permissions: vector_store:write (ingestion only)

Retrieval Service   ── service-account: retrieve-sa
  └─ Permissions: vector_store:read (query only, filtered by user context)

Generation Service  ── service-account: generate-sa
  └─ Permissions: llm_api:invoke (no direct vector store access)
  └─ Receives retrieved chunks via the retrieval service, not directly

Three services, three service accounts, three scoped permission sets. The generation service never touches the vector store directly — it receives pre-filtered, pre-authorized chunks from the retrieval service. A compromised generation service cannot exfiltrate the full vector store.


⚠ Production Gotchas

“We’ll add access control after we get the retrieval quality right”
Retrieval quality work (tuning chunk size, embedding models, similarity thresholds) generates many query examples. Those examples often span the full document corpus with no filtering. By the time you want to add access control, you have a pipeline that has never been tested with filters active, and adding filters now changes the retrieval behavior in ways that may break your quality benchmarks. Build access control into the pipeline before tuning retrieval quality — not after.

Namespace isolation without metadata means you still have a shared infrastructure problem
Pinecone namespaces are storage partitions — separate query spaces, not separate security boundaries at the infrastructure level. The Pinecone index itself is still a single IAM-controlled resource. If your application logic routes the wrong user query to the wrong namespace, the filtering doesn’t fire. Namespace isolation reduces risk; it does not eliminate the need for query-time authorization checks.

Embedding model updates break access control metadata if you’re not careful
When you re-embed your corpus with a new model, you typically truncate and re-index. If the access control metadata is only in the vector store (not also in your document store), re-indexing will drop it. Treat access control metadata as a property of the document, not of the embedding — store it in your document store and re-attach it during any re-indexing operation.

The retrieval service is the database for access control purposes
Teams that run careful security reviews on their application database often don’t apply the same review to their vector store. If the vector store contains documents from multiple users or sensitivity levels, it should receive the same security review as your primary database — network isolation, access logging, credential rotation, encryption at rest.


Quick Reference: RAG Access Control Decision Matrix

Your Architecture Minimum Required Controls
Single-tenant app Index-level access control (one index per app), service account isolation per pipeline stage
Multi-user app, shared corpus Metadata filtering at query time + post-retrieval authorization check
Multi-tenant SaaS Namespace/collection isolation per tenant + metadata filtering within namespace
Regulated data (PII, financial) PostgreSQL RLS or equivalent engine-level enforcement + full audit logging
Agent with autonomous retrieval All of the above + limit the agent’s retrieval service account to read-only, specific namespaces

Framework Alignment

Framework Reference Connection
OWASP LLM08 Vector and Embedding Weaknesses This episode is the access control dimension of LLM08
ISO 27001:2022 5.15 Access control Principle: access to data must be authorized, not assumed
NIST AI RMF MAP 2.1 Scientific basis for how AI capabilities interact with existing access control requirements
SOC 2 CC6.1 Logical access controls Evidence: vector store access control policies and query-time filtering
GDPR / Privacy Art. 25 (Data protection by design) Access control at retrieval is a technical privacy safeguard by default

Key Takeaways

  • Vector databases have no document-level access control by default — authorization must be built explicitly at ingestion, retrieval, and the application layer
  • The exposure is semantic, not structural: unauthorized documents are returned as semantically similar results, with no failed authentication to detect
  • Three enforcement points: tag documents at ingestion, filter at retrieval, verify at the application layer before context reaches the model
  • Separate service accounts for embedding, retrieval, and generation services — the generation service should never have direct vector store access
  • pgvector with PostgreSQL RLS is the strongest technical control — authorization enforced at the database engine, not in application code

What’s Next

The retrieval layer is one part of the pipeline. The full LLM pipeline — embedding service, retrieval service, generation service, tool execution layer — has an identity problem at every stage. In EP03, we build out the complete OIDC and workload identity architecture for an LLM pipeline, so each service has its own bounded identity with short-lived tokens instead of static credentials.

OIDC and Workload Identity for LLM Pipelines →

Get EP03 in your inbox when it publishes → subscribe