<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Sohan Maheshwar - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Sohan Maheshwar - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/sohan-maheshwar</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/sohan-maheshwar</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/sohan-maheshwar.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Wed, 11 Nov 2026 20:07:07 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Fine-grained authorization for Elasticsearch RAG with SpiceDB]]></title>
    <description><![CDATA[Department filters break the first time Engineering shares one document with Sales, so SpiceDB checks each retrieved chunk against a relationship graph before your Elasticsearch RAG pipeline passes it to the LLM.]]></description>
    <content:encoded><![CDATA[<p>You can implement fine-grained authorization in a relationship graph with SpiceDB. It doesn’t belong in your k-nearest neighbor (kNN) query, and it definitely doesn’t belong in your application code. Elasticsearch retrieves on relevance, and then each chunk is checked against the graph before the large language model (LLM) sees it. Sharing a document is one relationship write, and revoking it is one delete, but neither touches the index. Authorization itself runs in a deterministic LangGraph node that the agent can’t route around, because a model can be persuaded to grant access when it should not.</p><p></p><h2>Why coarse-grained permissions break in RAG pipelines</h2><p>Fine-grained access control can be difficult. Say, for example, that you've built a retrieval augmented generation (RAG) pipeline on Elasticsearch for an internal knowledge base. The requirements were reasonably straightforward: an AI chatbot front end that retrieves answers from 100K documents chunked into <code>dense_vector</code> embeddings. Permissions were fairly trivial, too, consisting of just a single <code>department</code> keyword filter in the kNN query.</p><p></p><p>Real-world requirements are often far more complex. Maybe the engineering team wants to share just one architecture document with the sales team, and one exec needs a personal exception to a document outside their department. How would you solve this in your RAG pipeline?</p><p></p><p>A stopgap solution can quickly become unwieldy. You could create a new <code>department</code> for each request, but then you end up with an explosion of roles that go stale the minute your system changes. You could also add a bunch of <code>if-else</code> statements, but that's a bad idea. Encoding security decisions in application code, which is neither auditable nor scalable, is how data breaches occur and is something you should protect against. This is especially true in enterprises and compliance-regulated industries, such as finance and public sector. </p><p></p><p>The latest <a href="https://genai.owasp.org/llm-top-10/">OWASP list for LLM risks</a> points out several industry-wide vulnerabilities specific to LLM applications, including: </p><p></p><ul><li><p>Sensitive Information Disclosure.</p></li><li><p>Excessive Agency.</p></li><li><p>Vector and Embedding Weaknesses.</p></li></ul><p></p><p>So how do you add fine-grained permissions to your RAG pipeline? The answer lies in relationship-based access control (ReBAC).</p><h2>ReBAC to the future: How relationship-based access control works</h2><p>In 2019, Google published <a href="https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/"><em>Zanzibar: Google's Consistent, Global Authorization System</em></a>, which describes the authorization system behind hundreds of Google services, including Calendar, Drive, Maps, Photos, and YouTube. If you've used any of these services, your requests have most likely been checked by Zanzibar.</p><p></p><p>The paper made waves in the world of identity and access management (IAM). It’s also widely credited with popularizing ReBAC, which was a departure from the models that most teams used at the time, including role-based access control (RBAC) and scoped tokens.</p><p></p><p>The core idea behind ReBAC is that permissions are determined by the <em>relationships</em> between <em>subjects</em> (users, agents, groups) and <em>resources</em> (documents, folders, projects). You can store those relationships as a graph: nodes are the objects, and edges are relationships like "viewer," "editor," or "owner." A permission check then boils down to a single question:</p><p></p><em>Is this </em><em><strong>actor</strong></em><em> allowed to perform this </em><em><strong>action</strong></em><em> on this </em><em><strong>resource</strong></em><em>?</em><p></p><p>Instead of force-fitting an access decision into a role, you answer that question by checking whether a path exists in the graph.</p><p></p><p>Here's a real-world example. When <code>user:jordan</code> creates <code>document:somedocument</code> in Google Docs, Zanzibar records that Jordan is a <code>writer</code> of the document as a relationship tuple:</p><p></p><p><code>document:somedocument#writer@user:jordan</code></p><p></p><p>If Jordan then shares the document with <code>user:alex</code> as a reader, another tuple is created:</p><p></p><p><code>document:somedocument#reader@user:alex</code></p><p></p><p>There's one more rule: every writer is also a reader. Conceptually, Zanzibar now sees a graph like this:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt17e633fa7848d51e/6aedf3de2f17b0dc5b38fa73/rebac-graph-somedocument.png" alt="ReBAC graph for somedocument: Alex and Sam are readers, Jordan is a writer, and reader includes writer" /><p>When Jordan tries to read <code>somedocument</code>, the system looks for a directed path from the document's <code>reader</code> relation to Jordan. There's no direct <code>reader</code> edge for Jordan, but <code>reader</code> includes <code>writer</code>, and Jordan is a writer. Now that the path is found, Jordan can read the document. If there were no path, Jordan would see the "You need access" screen that we all dread.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aada8404ab0a47b/6aedf45c371a18b70d610c8c/permission-check-path.png" alt="Permission check path: Jordan is a writer and reader includes writer, so Jordan can read somedocument" /><p>Now it's your turn. Can Alex <em>write</em> to the same document?</p><p></p><p>The next time you open (or can't open) a file in a Google product, you'll know what's happening behind the scenes.</p><h2>What does this have to do with my RAG pipeline?</h2><p>Zanzibar was designed for high-throughput, low-latency authorization checks over hierarchical groupings of organizational data. As it turns out, AI applications, such as agents and RAG pipelines, need exactly that. Look back at the requirements at the top of this article. Coarse-grained approaches, such as RBAC or scoped tokens, can't bend to per-document sharing or cross-department access, nor can they accommodate one-off exceptions at scale. The relationships between users and the resources they can access form an ever-evolving graph, which is exactly the problem ReBAC solves.</p><p></p><p>But unless you work at Google, you can't actually use Zanzibar. Fortunately, there are open source implementations of it. We're going to use <a href="https://github.com/authzed/spicedb">SpiceDB</a>, named after <em>Dune</em>’s famous line, “The spice must flow.” The techniques mentioned here aren’t RAG-specific; they can also be used to add permissions to your enterprise applications and AI agents, in addition to any other resources that you want to protect.</p><h2>Define a SpiceDB schema, relations, and permissions</h2><p>As the name suggests, ReBAC is built on the relationships between the objects in your system. So, when adding permissions to your application, the first step is to define a SpiceDB <em>schema.</em>  This describes the types of objects in your application and how those objects can relate to one another. It also defines the permissions derived from those relationships. For our RAG pipeline, it looks like this:</p>definition user {}

definition department {
    relation member: user
}

definition document {
    relation owner: user
    relation viewer: user | department#member

    permission view = viewer + owner
    permission edit = owner
}<p>There’s a lot to unpack here:</p><p></p><ul><li><p>The three object types are <code>user</code>, <code>department</code>, and <code>document</code>.</p></li><li><p>The relations are as follows:</p></li><ul><li><p>A <code>user</code> can be a <code>member</code> of a <code>department</code>.</p></li><li><p>A <code>user</code> can be the <code>owner</code> of a <code>document</code>.</p></li><li><p>A <code>user</code> can be a <code>viewer</code> of a <code>document</code>.</p></li></ul><li><p>You can <code>view</code> a document if you're a <code>viewer</code> or its <code>owner</code>. And <code>viewer</code> permissions can be granted in one of two ways: to a specific user <em>or</em> to every <code>member</code> of a department (<code>department#member</code>).</p></li><li><p>Only an <code>owner</code> of a <code>document</code> can <code>edit</code> it.</p></li></ul><p></p><p>The schema represents the shape of your system, and it rarely changes. </p><p></p><p>In SpiceDB, a working permissions system is the combination of a schema and relationships. A <em>relationship</em> is a specific instance of a relation. For example, "<code>user</code> jordan is an <code>owner</code> of <code>document</code> readme" is written as a relationship tuple:</p><p></p><p><code>document:readme#owner@user:jordan</code></p><p></p><p>What if you wanted to say that <code>alice</code> is part of <code>engineering</code>, and everyone in <code>engineering</code> can view <code>eng-arch-001</code>? That's also just relationships:</p><p></p><ul><li><p><code>department:engineering#member@user:alice</code> ("Alice is a member of engineering.")</p></li><li><p><code>document:eng-arch-001#viewer@department:engineering#member</code> ("Every engineering member can view this doc.")</p></li></ul><p></p><p>So if <code>alice</code> clicks a link to view <code>eng-arch-001</code>, SpiceDB walks the graph like this:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt20892ab5e69d902c/6aedf5ba91a6e8df6c0c88c7/spicedb-permission-check.png" alt="SpiceDB permission check walking from document eng-arch-001 viewer to engineering member alice, allowed" /><p>If no path exists, access is denied.</p><p></p><p>That's how a permission check works in SpiceDB. Before performing any action, you call the <code>CheckPermission</code> API:</p>from authzed.api.v1 import (
    CheckPermissionRequest,
    CheckPermissionResponse,
    Client,
    ObjectReference,
    SubjectReference,
)
from grpcutil import insecure_bearer_token_credentials

client = Client("localhost:50051", insecure_bearer_token_credentials("devtoken"))

response = client.CheckPermission(
    CheckPermissionRequest(
        resource=ObjectReference(object_type="document", object_id="eng-arch-001"),
        permission="view",
        subject=SubjectReference(
            object=ObjectReference(object_type="user", object_id="alice")
        ),
    )
)
allowed = (
    response.permissionship
    == CheckPermissionResponse.PERMISSIONSHIP_HAS_PERMISSION
)<p>We'll use this same check in our RAG pipeline to make sure that users only get answers based on data they're allowed to see.</p><h2>When access rules change, write a relationship not a role</h2><p>Remember how the requirements for our RAG project changed? In a role-based system, that would mean refactoring your roles or burying more authorization logic in your code. With ReBAC and SpiceDB, every new access rule is just a new relationship:</p><p></p><p></p><p><strong>RBAC / scoped tokens</strong></p><p><strong>ReBAC with SpiceDB</strong></p><p>Access expressed as</p><p>Roles assigned to users</p><p>Relationships between subjects and resources</p><p>A permission check asks</p><p>Does this user hold the role?</p><p>Does a path exist in the graph?</p><p>One-off exception for one person</p><p>New role</p><p>One relationship write</p><p>Sharing with another team</p><p>New role per combination</p><p>One relationship write via <code>department#member</code></p><p>When access changes</p><p>Refactor roles or edit filters</p><p>Write or delete a relationship</p><p>Where the logic lives</p><p>Application code</p><p>SpiceDB</p><p></p><p></p><p>If the engineering team wants to share a document with the sales team, you add this relationship:</p><p></p><p><code>document:eng-arch-001#viewer@department:sales#member</code></p><p></p><p>And you can create an individual exception for an exec, like so:</p><p></p><p><code>document:sales-proposal-001#viewer@user:ceo</code></p><p></p><p>In RBAC, you'd be inventing new roles or rewriting filters, but in SpiceDB, each of these is a single <code>WriteRelationships</code> call. Here's the sales share:</p>from authzed.api.v1 import (
    ObjectReference,
    Relationship,
    RelationshipUpdate,
    SubjectReference,
    WriteRelationshipsRequest,
)

client.WriteRelationships(
    WriteRelationshipsRequest(
        updates=[
            RelationshipUpdate(
                operation=RelationshipUpdate.Operation.OPERATION_TOUCH,
                relationship=Relationship(
                    resource=ObjectReference(object_type="document", object_id="eng-arch-001"),
                    relation="viewer",
                    subject=SubjectReference(
                        object=ObjectReference(object_type="department", object_id="sales"),
                        optional_relation="member",
                    ),
                ),
            )
        ]
    )
)<p>This requires no new roles or code changes, and it needs no reindexing. <code>OPERATION_TOUCH</code> makes the write idempotent; that is, it creates the relationship, or, if it already exists, leaves it alone.</p><h2>Wire SpiceDB into your Elasticsearch RAG pipeline</h2><p>Let's bring this all together. When you index chunks into Elasticsearch, you store metadata alongside each embedding that says which document it came from. In our repo, that's a <code>doc_id</code> field. It's the same ID that SpiceDB uses for the <code>document</code> resource, and it's what connects the two systems.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41b6f8bb95500bd1/6aedf6a21a6f6e0bc853d9f9/rag-pipeline-with-spicedb.png" alt="RAG pipeline with SpiceDB: Mistral embeddings indexed in Elasticsearch, chunks checked by CheckPermission" /><p>Here's the query flow:
</p><ol><li><p>The user's query goes through an embedding model (Mistral, in our repo) to generate a query embedding.</p></li><li><p>Elasticsearch runs a kNN search to retrieve the most relevant chunks. </p></li><li><p>Before our documents are passed to the LLM, we send each chunk's <code>doc_id</code> and the user's ID to SpiceDB.</p></li><li><p>We drop every chunk that SpiceDB determines the user can't view, based on the defined rules.</p></li><li><p>The permitted documents are passed into the prompt. In the case of agentic RAG, the relevant documents are passed as the output of the tool to the LLM.</p></li></ol><p></p><p>This pattern is called <em>post-filter authorization</em>: retrieve first, and then filter. The check happens before the LLM to ensure that unauthorized documents aren’t passed via the context window. That's the security boundary. Anything that reaches the model can end up in its answer.</p><p></p><p>Here's the retrieval step (abridged) from the repo:</p># query_embedding comes from the Mistral embedding model
response = es_client.search(
    index="documents",
    knn={
        "field": "embedding",
        "query_vector": query_embedding,
        "k": 5,
        "num_candidates": 50,
    },
    source=["doc_id", "title", "content"],
)

documents = [
    Document(
        page_content=hit["_source"]["content"],
        metadata={"doc_id": hit["_source"]["doc_id"], "title": hit["_source"]["title"]},
    )
    for hit in response["hits"]["hits"]
]<p>Notice that there's no permission logic in the query. Elasticsearch finds the relevant content, and SpiceDB decides who gets to see it.</p><p></p><p>Next comes the authorization step. Here's the repo's <code>authorization_node</code>. It hands the retrieved documents to <code>SpiceDBAuthorizer</code> from the <code>langchain-spicedb</code> package, which checks all of them against SpiceDB in a single <code>CheckBulkPermissions</code> call:</p>async def authorization_node(state: AgenticRAGState) -&gt; dict:
    """
    Deterministic authorization node - ALWAYS runs, cannot be bypassed.
    """
    authorizer = _get_authorizer()

    logger.info(
        "Starting authorization",
        extra={
            "subject_id": state["subject_id"],
            "document_count": len(state["retrieved_documents"]),
        },
    )

    result = await authorizer.filter_documents(
        documents=state["retrieved_documents"],
        subject_id=state["subject_id"],
    )

    logger.info(
        "Authorization results",
        extra={
            "authorized": result.total_authorized,
            "denied": len(result.denied_resource_ids),
            "denied_doc_ids": result.denied_resource_ids,
        },
    )

    return {
        "authorized_documents": result.authorized_documents,
        "denied_count": len(result.denied_resource_ids),
        "authorization_passed": result.total_authorized &gt; 0,
        "messages": [
            SystemMessage(
                content=f"Authorization: {result.total_authorized}/{result.total_retrieved} documents authorized"
            )
        ],
    }<p><code>_get_authorizer()</code> sets the authorizer up once to check the <code>view</code> permission on <code>document</code> resources for <code>user</code> subjects, using each document's <code>doc_id</code> metadata as the resource ID. Only documents with a confirmed path in SpiceDB come back in <code>result.authorized_documents</code>. A document with no <code>doc_id</code>, or an individual check that errors, is dropped. If SpiceDB can't be reached at all, the call raises and the pipeline stops before generation.</p><h2>What about Elasticsearch document-level security?</h2><p><a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level">Elasticsearch has document-level security built in</a>, and it's a great fit when access can be expressed as a query over fields in the document, like <code>department: engineering</code>. It gets more difficult when access depends on relationships like the ones mentioned in the introduction, such as per-user exceptions or sharing through groups. Other examples include access inherited from folders and orgs. You'd have to denormalize that graph into every document and map those to defined RBAC roles. You’d also have to reindex whenever a share changes. SpiceDB keeps the graph in one place and gives the same answer to your RAG pipeline and your app, as well as to your agents.</p><h2>Never let the agent decide whether to authorize</h2><p>The pipeline follows a clear shape: retrieve → authorize → generate (with an optional reasoning loop), built as <a href="https://www.langchain.com/langgraph">LangGraph</a> nodes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4d8609f02c83e18/6aedf798371a183ec5610cda/fine-grained-authorization.png" alt="Fine-grained authorization runs between retrieval and generation, so only authorized chunks reach the LLM" /><p>The core rule is simple: the agent may reason about what to retrieve or what to generate. It may even reason about how to explain a denial. But it must never reason about <em>whether</em> authorization applies.</p><p></p><p>LLMs are inherently probabilistic. If the model decides whether to check permissions, a well-crafted prompt injection can persuade it not to do so, thus leaking data. Enforcement lives outside the model in a deterministic node that the agent can't route around. That node must also block access by default in the event of a failure. An error or a missing path means no access.</p><p></p><p>Here's how the three nodes are wired up, with an entry point, the edges, and an endpoint using LangGraph:</p>from langgraph.graph import END, StateGraph

workflow = StateGraph(AgenticRAGState)

# Add nodes
workflow.add_node("retrieve", retrieval_node)
workflow.add_node("authorize", authorization_node)  # ALWAYS runs
workflow.add_node("generate", generation_node)

# Define flow - start directly at retrieval
workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "authorize")  # Deterministic auth
workflow.add_edge("authorize", "generate")
workflow.add_edge("generate", END)

graph = workflow.compile()<p>There's no edge from <code>retrieve</code> to <code>generate</code>. The only road to the LLM runs through <code>authorize</code>. The repo adds an optional <code>reason</code> node for retries (the agentic part), but every path still passes through authorization first.</p><p></p><p>Each LangGraph node is just a function. You saw <code>authorization_node</code> in the previous section, and here's <code>generation_node</code>:</p>def generation_node(state: AgenticRAGState) -&gt; dict:
    """Generate final answer incorporating authorization context.

    This node creates the final response, ensuring transparency
    about what information was accessible and what was denied.
    """
    with log_node_execution(
        logger,
        "generation",
        {
            "subject_id": state["subject_id"],
            "authorized_count": len(state["authorized_documents"]),
            "denied_count": state["denied_count"],
        }
    ):
        llm = get_llm()

        # Format documents with clear boundaries
        if state["authorized_documents"]:
            doc_parts = []
            for i, doc in enumerate(state["authorized_documents"], 1):
                doc_parts.append(
                    f"--- Document {i} ---\n"
                    f"Title: {doc.metadata['title']}\n"
                    f"Content: {doc.page_content}\n"
                    f"--- End Document {i} ---"
                )
            docs_text = "\n\n".join(doc_parts)
        else:
            docs_text = "No authorized documents available."

        prompt = ChatPromptTemplate.from_messages(
            [
                ("system", GENERATION_PROMPT),
            ]
        )

        chain = prompt | llm

        result = chain.invoke(
            {
                "query": state["query"],
                "authorized_count": len(state["authorized_documents"]),
                "denied_count": state["denied_count"],
                "documents": docs_text,
            }
        )

        logger.info(
            "Generated answer",
            extra={
                "answer_length": len(result.content),
            },
        )

        return {
            "answer": result.content,
            "messages": [AIMessage(content=f"Answer: {result.content}")],
        }<p>The generation node reads only <code>authorized_documents</code>, so denied content never reaches the prompt. <code>GENERATION_PROMPT</code> tells the model to answer only from those documents. It also passes along the denied count, so the answer can point out that some information wasn't accessible (instead of pretending it doesn't exist). It’s important not to return details about rejected documents, since those details could leak  information that the user isn’t authorized to access.</p><h2>Taking fine-grained authorization to production</h2><p>How do you go from a proof of concept to a production-grade implementation? Here are our recommendations:</p><p></p><ul><li><strong>Overfetch to recover post-filter recall. </strong>Post-filtering can leave the LLM short on context. For example, the user can ask about “microservices patterns,” and if four of the top five hits point to documents they can’t see, the LLM has a single document to work with. The fix is to overfetch: retrieve k × N candidates (where <em>k</em> is how many documents you want and <em>N</em> is a multiplier that you tune), and run the permission check. Then, keep the first k that survive. Note that overfetching improves the odds but doesn’t guarantee k survivors. num_candidates only has to change if the new k would exceed it; if you leave it unset, it defaults to 1.5 × k (or 1.5 × size when k isn’t set) and scales on its own. <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#:~:text=search%20request%20size.-,num_candidates,-(Optional%2C%20integer)%20The">num_candidates is capped at 10,000</a>, so overfetching has a ceiling. If a user can see so little of the corpus that you’d need more than that, SpiceDB has a <code>LookupResources</code> API which finds all resources of a given type that a specific subject can access. It works best when the result set is moderate: SpiceDB’s docs warn that it gets expensive above roughly 10,000 results. In that case, the docs recommend post-filtering with CheckBulkPermissions. You’d typically use this API to get the list of document IDs that the user can access and then pass them as a terms pre-filter in the kNN query’s filter parameter (up to the default index.max_terms_count value of 65,536 terms).</li></ul><p></p><ul><li><strong>Add a coarse-grained metadata pre-filter.</strong> Authorization is rarely the only constraint in a RAG pipeline. You may also want to restrict results by department or recency or by classification. Elasticsearch's kNN <code>filter</code> applies those conditions during the vector search itself, so you don't waste top-<em>k</em> slots on documents that you'd throw away. SpiceDB still makes the fine-grained call afterward:</li></ul>response = es_client.search(
    index="documents",
    knn={
        "field": "embedding",
        "query_vector": query_embedding,
        "k": 5,
        "num_candidates": 50,
        # Coarse pre-filter: this assistant never retrieves confidential docs
        "filter": {"terms": {"classification": ["public", "internal"]}},
    },
    source=["doc_id", "title", "content"],
)<p></p><ul><li><strong>Keep relationships in sync with the source of truth.</strong> In the repo, a setup script seeds the relationships; but in production, they come from wherever access is actually managed, like your document system or HR directory. They could also come from your identity provider. Write a relationship when a document is shared or someone joins a team, and delete it when access is revoked. Because permissions live in SpiceDB and not in the index, none of this requires reindexing.</li></ul><p></p><ul><li><strong>Deploy SpiceDB and Elasticsearch.</strong> The repo runs SpiceDB and Elasticsearch in local Docker containers, with SpiceDB storing relationships in memory. A production deployment of SpiceDB needs a persistent datastore, like PostgreSQL. You also need to run SpiceDB somewhere. There are currently three main deployment options:</li><ul><li><p><strong>S</strong><strong>piceDB, self-hosted (open source).</strong> Run and manage it entirely on your own infrastructure.</p></li><li><p><a href="https://authzed.com/products/authzed-cloud"><strong>AuthZed Cloud.</strong></a> Managed, self-service, pay-as-you-go SpiceDB with built-in enterprise features, like audit logging.</p></li><li><p><a href="https://authzed.com/products/authzed-dedicated"><strong>AuthZed Dedicated.</strong></a> A private, single-tenant deployment provisioned in your chosen cloud provider and region.</p></li></ul></ul><p>Similarly, Elasticsearch can be deployed alongside, via Elastic Cloud Hosted, Elastic Cloud Serverless, and on-prem distributions, with the <a href="https://www.elastic.co/docs/get-started/deployment-options">options outlined in the documentation</a>.</p><h2>Elasticsearch retrieves, and SpiceDB authorizes</h2><p>Elasticsearch finds the most relevant content, and SpiceDB decides who's allowed to see it. A deterministic authorization node makes sure that the user gets responses based only on the information they’re allowed to access.</p><p></p><p>To try it yourself:</p><p></p><ol><li><p>Clone the <a href="https://github.com/authzed/examples/tree/main/agentic-rag-authorization">sample repository</a>, and follow the README to start SpiceDB and Elasticsearch with Docker Compose.</p></li><li><p>Ask the same question as two different users, and compare what each one gets back.</p></li><li><p>Read the <a href="https://zanzibar.tech">annotated Zanzibar paper</a>.</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/fine-grained-authorization-elasticsearch-rag-spicedb</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/fine-grained-authorization-elasticsearch-rag-spicedb</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[Integrations]]></category>
    <dc:creator><![CDATA[Sohan Maheshwar,Carly Richmond]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltabe7f972f0f76168/6aedf29b1f7c3bdd2ef37a19/rag-pipeline-spicedb.png" length="0" type="image/png"/>
    <pubDate>Wed, 11 Nov 2026 16:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>