Zuplo
API Security

RBAC Isn't Enough for AI Agents

Nate TottenNate Totten
June 8, 2026
6 min read

One agent acts for hundreds of users across thousands of resources. A role or a coarse allowlist can't express 'this agent, for Alice, may read document 42.' Authorization for agents has to be per-resource and relationship-aware.

I gave an internal agent access to our docs tooling last month and watched it do something a human never would: in one session it touched files belonging to eight different people, on behalf of four different requesters, in under a minute. That is the shape of agent traffic, and it broke the mental model I had been carrying for authorization. A role answers “what may this caller do.” An agent has no fixed answer, because it acts for whoever asked it, against whatever resource the task happens to reach.

Role-based access control assumes a stable subject with a stable set of permissions. Agents violate both halves. The grant you actually need is not “the agent may call the database” or “the agent has the editor role.” It is “this agent, acting for Alice, may read document 42, and nothing else Alice can’t read.” A role can’t say that, and neither can a coarse allowlist. This is the same thing Anthropic landed on when it reframed an allowlist as a capability grant: “this agent may reach this server” is far broader than the per-call decision you meant to make.

Use this approach if you're:
  • You expose an API or MCP server to agents that act on behalf of many different end users
  • Your authorization model is a role or a static allowlist and you suspect it's too coarse for per-resource decisions
  • You need a per-action audit trail of authorization decisions for compliance
  • You're weighing RBAC against ABAC or relationship-based access control for agent traffic

Three authorization models and where each breaks

RBAC groups permissions into roles and assigns roles to subjects. It is fine when the subject is a person whose job describes their access. It falls apart the moment one subject acts for many principals against per-resource scopes, because you would need a role per (user, resource) pair, which is not a role anymore.

ABAC, attribute-based access control, evaluates a policy over attributes of the subject, resource, and environment. It is more expressive, but the per-resource relationships an agent needs (“Alice is a viewer of document 42 because she’s in the team that owns folder 7”) tend to live in your application data, not in a token’s attributes. You end up carrying the whole relationship graph into every request or rebuilding it in policy.

ReBAC, relationship-based access control, models authorization as a graph of relationship tuples and answers each check by walking that graph. This is the model Google described in its Zanzibar paper, the system behind Drive, Calendar, and YouTube permissions. A check for “can alice view document 42” resolves by traversing edges like user:alice is a viewer of document:42, and document:42 is in folder:7. OpenFGA is the open-source implementation of that model, and its core question is exactly the agent question: given this user, this relation, and this object, is the answer yes.

ModelDecision unitFits agents?
RBACRole to subjectNo, subject and resource scope are both unstable
ABACPolicy over attributesPartial, relationship data has to be carried in
ReBACPer-resource relationshipYes, the check is per (user, relation, object)

Why per-call checks shrink an agent’s blast radius

A coarse grant is a capability the agent keeps for the whole session, and a poisoned tool response or a confused loop spends it freely. A per-call relationship check is re-evaluated on every action against the resource that action names, so a compromised agent can only reach what the current principal could already reach. The principal here is the end user the agent is acting for, read from the verified token on the request, not an identity the agent asserts for itself. That is the same containment argument behind putting a rate limit on every MCP route and behind scanning tool responses for prompt injection: the boundary enforces a fresh, narrow decision instead of trusting a broad, durable grant. It also produces the artifact regulators increasingly ask for. A per-action decision logged with the principal, the relation, and the object is an audit trail. “The agent had the editor role” is not.

Tool curation today, per-resource ReBAC next

The least-privilege control that is MCP-native and ships today is curation plus scoping, not relationship checks. With the Zuplo MCP Gateway you publish a hand-picked subset of an upstream’s tools per route, so the agent only sees the tools you chose, and you bind every token to the server it was minted for so it can’t be replayed elsewhere. That is your first step.

Per-resource authorization is the deeper layer, and Zuplo gives you a composable piece for it rather than a turnkey button. The openfga-authz-inbound policy (export OpenFGAAuthZInboundPolicy) runs a Zanzibar-style ReBAC check at the gateway boundary on the way in. A few honest caveats:

  • It is in beta and an enterprise feature.
  • It is the enforcement point, not the store. You bring an external OpenFGA or Okta FGA instance; the policy queries it. The apiUrl, storeId, and authorizationModelId are required because the relationship graph lives in your FGA deployment, not in Zuplo.
  • It is a general API-gateway inbound policy. It is not wired into the MCP Gateway path as a one-click feature, so treat it as a platform capability you compose onto a route, including an MCP route, rather than a built-in MCP setting.

The most common way to drive the check is setContextChecks from a custom inbound policy, where you compute the tuple from the live request: the principal the agent is acting for, the relation, and the resource the call names.

TypeScriptts
// modules/agent-authz.ts
import {
  OpenFGAAuthZInboundPolicy,
  HttpProblems,
  RuntimeError,
  type ZuploRequest,
  type ZuploContext,
} from "@zuplo/runtime";

export async function canReadDocument(
  request: ZuploRequest,
  context: ZuploContext,
) {
  if (!request.params?.documentId) {
    throw new RuntimeError("Document ID not found in request");
  }
  if (!request.user?.sub) {
    return HttpProblems.forbidden(request, context, {
      detail: "User not found",
    });
  }

  // The principal is whoever the agent acts for on THIS call, not the agent
  // itself, so the check narrows to what that user could already reach.
  OpenFGAAuthZInboundPolicy.setContextChecks(context, {
    user: `user:${request.user.sub}`,
    relation: "viewer",
    object: `document:${request.params.documentId}`,
  });

  return request;
}

The policy then asks your FGA store whether that tuple holds and rejects the call if it doesn’t. The grant is no longer “the agent may read documents.” It is “this principal may view this document,” decided per call.

OpenFGA Authorization Policy Reference

Full reference for the openfga-authz-inbound policy, including setContextChecks, credentials, and the required external FGA store configuration.

Common mistake:

Binding the check to the agent’s own identity instead of the principal it’s acting for rebuilds the coarse grant you were trying to escape. The agent inherits a union of everyone’s access. Compute the tuple from the request’s acting principal, not from the service account the agent authenticated with.

When to add per-resource checks

If you ship one thing this quarter, ship tool curation and audience-bound tokens on your MCP routes. That is the least-privilege step that is native and available now. When a single agent starts acting for many users against per-resource scopes, and “the agent has a role” stops being a truthful answer, that is the signal to layer relationship checks on top, with an external FGA store behind the beta policy. RBAC was built for stable subjects. Agents aren’t stable subjects, and the authorization model has to follow.

The Zuplo MCP Gateway is in public beta and available now on every plan, including the free one. Spin up a free project and start adding per-call authorization to your MCP routes today.