Chapter 2. Enterprise Access Control: securing RAG with RBAC/ABAC
A Kovcheg operations clerk asks the assistant: "What were the terms on the last deal with this customer?" The assistant honestly searches the RAG index, finds a document from the legal department — and hands it over. Problem: the clerk has no access to that document in SharePoint. They didn't hack anything; they just asked the bot, and the bot isn't a rights subject. The retriever found the most relevant hit, the model summarized it. This is how AI produces privileged leakage: data the person has no access to at the source flows out through the assistant anyway.
This is a fundamental defect of naive RAG: indexing collapses data at different access levels into a single vector index, and semantic search doesn't know about permissions. This chapter is about the identity plane: RAG must return answers strictly within the rights the user actually has in the source systems (Confluence, Jira, SharePoint, PostgreSQL) — and that must be provable.
The customer's business goal
A Kovcheg user gets from RAG exactly what they're allowed to see in the source systems — no more. Promises to the business:
- No answer contains data the asker lacks access to at the source.
- Access revocation at the source propagates into RAG within minutes, not days.
- When a regulator or auditor asks "why did this employee see this," the answer lies in the permissions, not in the retriever's luck.
Driver: threat or regulator
- Confused deputy: the AI acts under its own identity with the full permissions of the index, rather than on behalf of the user. A classic internal-leak vector.
- OWASP LLM: LLM02 (Sensitive Info Disclosure), LLM06 (Excessive Agency — when RAG tools reach into systems).
- GDPR / banking secrecy: access to PII without a legal basis is a violation even inside the company.
- AI Act audit: access to high-risk data must be logged and justified.
Architectural pattern
Identity-Aware Hybrid Filtering RAG — the 2026 enterprise-architecture consensus: don't choose between fast pre-filtering and precise authz — combine both:
JWT(user: groups, roles, tenant_id)
│
▼
[1] Pre-filter in the vector DB by ACL metadata (recall, cheap)
│ candidate chunks
▼
[2] FGA post-check: CheckBulkPermissions against each chunk's source document
│ permitted chunks (precision, ReBAC/ABAC)
▼
[3] LLM → [4] OPA post-filter on the answer (field masking, forbidden combinations)
The pre-filter cheaply cuts out the bulk; the FGA service gives a precise check where the payload filter isn't enough (complex relationships, ABAC context). The user's rights penetrate the retrieval itself, rather than being layered onto the output after the fact.
Engineering stack & providers
- Vector DB with payload filters: Qdrant, Pinecone, PostgreSQL (pgvector).
- Identity: OpenID Connect (OIDC), Keycloak; JWT with
groups,roles,tenant_id. - Fine-Grained Authorization (ReBAC/ABAC): OpenFGA, SpiceDB, Cerbos, Auth0 FGA —
CheckBulkPermissionsin the retrieval path. - Output-side policy: OPA (Rego).
- Orchestration: LlamaIndex / LangChain.
Engineering implementation
### Step 1. ACL as chunk metadata + version
At indexing time, each chunk's payload gets a normalized ACL array and an acl_version. The version is the key to instant invalidation (see synchronization below).
payload = {
"tenant_id": doc.tenant_id,
"allowed_groups": doc.acl_groups, # from the source system
"source_doc_id": doc.id, # for FGA post-check
"acl_version": doc.acl_version,
}
### Step 2. Pre-filter from the JWT security context
flt = {"must": [
{"key": "tenant_id", "match": {"value": user.tenant_id}},
{"key": "allowed_groups","match": {"any": user.groups}},
]}
candidates = qdrant.search(query_vec, query_filter=flt, limit=50)
### Step 3. FGA post-check against the source
allowed = fga.batch_check(
user=f"user:{user.id}",
relation="viewer",
objects=[f"doc:{c.payload['source_doc_id']}" for c in candidates],
)
chunks = [c for c in candidates if allowed[c.payload["source_doc_id"]]]
### Step 4. ACL sync — event-driven, not batch
Subscribe to permission-change webhooks from the source systems; permission changes reconcile within minutes. Bump the acl_version → stale chunks are invalidated. A nightly batch sync is unacceptable here: a day with a revoked access still valid is a day of leakage.
### Step 5. OPA at the output
The final answer and its citations pass through policy: masking of individual fields, banning dangerous combinations, redacting reference-link metadata.
Where it breaks
- ACL drift is the main risk. Permissions change at the source, the index lags → you hand over what's already forbidden. The only fix is event-driven sync plus real-time FGA checking (pre-filtering against the index always lags by the sync delay).
- Leakage through aggregation. Chunks permitted individually reveal something forbidden in aggregate. Neither pre-filter nor FGA sees this — it needs OPA/business-rule logic on top.
- Citation metadata. Content is filtered, but the source's title/link in the answer reveals the mere existence of the document. Clean up citations too.
- FGA latency.
CheckBulkPermissionsacross dozens of chunks adds delay; balance pre-filter depth against post-check volume. - ABAC context. "Access only during business hours / only for your own region / only with a business justification" doesn't fit cleanly into a payload — it moves into FGA/OPA, adding complexity.
Standards and mapping
- ISO/IEC 42001: access controls for AI system data and systems.
- EU AI Act: Art. 10 (data governance), access logging for high-risk.
- OWASP LLM: LLM02, LLM06.
- NIST AI RMF: Govern/Map — data and access control.
Lab and artifact
Set up Qdrant with payload ACLs and acl_version for Kovcheg, OIDC via Keycloak, pre-filter by tenant_id+groups, an OpenFGA relationship model + batch_check in the retrieval path, webhook-based access-revocation sync, OPA at the output. Test: two users with different permissions ask the same question from the introduction — compare the results; revoke access and measure reconciliation time. Artifact: FGA model + rego policies + a "role × document → access" test matrix + a sync-lag metric.
Maturity checklist
- L1: a single index filtered by
tenant_id. - L2: pre-filter by groups from the JWT + FGA post-check against the source, OPA at the output.
- L3: event-driven ACL sync (minutes), protection against aggregation and citation leaks, ABAC context, an audit trail for every access (ties to ch. 4).
Sources
- [Document-Level RBAC for RAG (2026 guide, Truto)](https://truto.one/blog/how-to-maintain-document-level-rbac-in-enterprise-rag-pipelines/)
- [Fine-Grained Authorization for RAG with OpenFGA (Auth0)](https://auth0.com/ai/docs/intro/authorization-for-rag)
- [The Right Approach to Authorization in RAG (Oso)](https://www.osohq.com/post/right-approach-to-authorization-in-rag)
- [RAG with Access Control (Pinecone)](https://www.pinecone.io/learn/rag-access-control/)
How it actually works — engineering breakdowns
Standalone howto from practice, showing this control plane on real code and a working artifact.
- Retrieval Layer for Book-as-context: Why Vector-only BreaksRetrieval layer (vector + BM25) that access control is layered on top of.
Read next
Putting AI into production under regulatory risk?
Designing the control plane for your system: privacy, access, guardrails, audit, EU AI Act / ISO 42001 compliance — as working architecture, not a policy PDF.
Email meThe transition engine
Next Move Engine — the system that takes a team to an autonomous delivery loop.
Next Move Engine →