Securing Agentic IAM: Implementing Least-Privilege at the Protocol Layer

Written by

in

# Securing Agentic IAM: Implementing Least-Privilege at the Protocol Layer

![Securing agentic IAM — the OnlineCloud security architect reference architecture](https://www.onlinecloud.dev/assets/hero-profile-BlpwYfK_.png)

*By the security engineering team at OnlineCloud — a US-based Agentic AI & MCP engineering studio*

—

## 1. Introduction: The Vulnerability of Shared Service Accounts

Every agentic deployment I audit has the same origin story. A team wants an agent to read tickets, draft replies, and update a CRM. The fastest path to a demo is a service account: one long-lived credential, provisioned once, stuffed into an environment variable, and handed to the agent. It works. It demos beautifully. And it quietly collapses the entire distinction between *who the user is* and *what the system can do*.

A shared service account is a single identity that represents nobody. When the agent executes `update_crm(account_id, note)`, the downstream system sees a service principal with write access to the whole table — not the support representative who asked for the change, not the entitlement scope of the requesting tenant, not the disposition of the customer whose record is being touched. The service account is a master key with a friendly name, and in an agentic system, a master key is held by a stochastic, prompt-injectable principal that reads untrusted content and then decides what to call next.

This is the crux. In a traditional backend, the service account is invoked by code whose control flow an engineer wrote and reviewed. In an agent, the “control flow” is generated at runtime from a conversation that may contain a poisoned document, a malicious tool result, or a user who simply words a request differently than the prompt’s author imagined. The model does not need to be adversarial for the blast radius to be catastrophic; it only needs to be *steered*. Indirect prompt injection turns a document into an instruction, and a shared credential turns that instruction into an authorized action.

The failure modes compound in three ways. **Attribution loss:** the audit trail points at `svc-agent-prod`, which tells an investigator nothing about which human, which task, or which approval is accountable. **Privilege monotonicity:** service accounts accumulate permissions over months and are almost never reduced, so the credential’s authority only grows while the agent’s actual needs stay fixed. **No revocation granularity:** you cannot revoke one user’s session without breaking the agent for everyone, so a suspected compromise becomes an all-or-nothing shutdown.

Least privilege at the protocol layer is the corrective. It means that no credential the agent holds is broader than the task in front of it, that every request carries an identity traceable to a human, that authorization is decided per tool call by policy rather than by connection, and that every decision leaves a replayable record. The Model Context Protocol (MCP) gives us the right substrate — it is the contract layer between the LLM and your business logic — but the security guarantees must be engineered into that layer. The next four sections build them, and the last assembles them into a reference architecture.

## 2. OAuth 2.1 for Agents: Implementing Scoped Tokens and Audience Enforcement

The first fix is to stop treating authentication as something that happens once at connect time. For agent workloads I standardize on **OAuth 2.1 with the Authorization Code flow and PKCE**, with a genuinely short access-token TTL and single-use, rotating refresh tokens. The details of the code exchange are well understood; the part that determines whether your agent is actually contained is what the token *contains* and what it is *valid for*.

Scopes are where least privilege is won or lost. The cardinal sin is a coarse scope — `data:all`, `resources:read_write`, `crm:*`. A scope should read like the smallest sentence describing the smallest useful action: not `orders`, but `orders:read:own`; not `refunds`, but `refunds:create:under_500`. Design scopes as the Cartesian product of (operation, resource class) and mint one per pair the agent genuinely needs. If a tool only reads a customer’s order history, the token must not carry a write scope “just in case,” because “just in case” is the blast radius an injected instruction will happily exercise.

Two mechanisms turn scopes from a label into an enforced boundary.

**Scope-to-tool mapping at the MCP server.** Each tool declares the scopes its invocation requires, and the server rejects any call whose token lacks them *before* the handler executes. This is a declarative allowlist enforced in exactly one place, so adding a tool forces an explicit authorization decision rather than an implicit inheritance:

“`json
{
“tools”: {
“get_order_status”: {
“required_scopes”: [“orders:read:own”, “shipments:read:own”],
“audience”: “mcp://ledger”
},
“create_refund”: {
“required_scopes”: [“refunds:create:under_500”],
“human_checkpoint”: “refund > $500”
}
}
}
“`

**Audience enforcement.** Scope answers “may this caller invoke this tool?” Audience answers “was this token *meant* for me?” The `aud` claim must equal the specific MCP server’s identifier, and the server must validate it on every request alongside `iss`, `exp`, and signature. Without audience binding, a token minted for a low-risk analytics server is a valid credential at your ledger server — a confused-deputy attack that token reuse makes trivial. Enforce one audience per server, reject `*`, and require the caller to perform **RFC 8693 token exchange** to obtain a server-specific token rather than reusing a broad one.

The token flow I recommend for agent tasks is explicit delegation, not impersonation. The orchestrator exchanges the user’s login token for a **task-scoped delegation token** with narrower scopes, a TTL measured in minutes, an explicit `aud` of the single MCP server, and a `delegation` claim chain naming the initiating user and each agent hop. The model never holds the user’s token; it holds a perishable proxy that expires when the task does. Every request is authenticated per call — a session authorized at 09:00 is not authorized after a revocation at 09:01 — and validation order is fixed: signature, issuer, audience, expiry, then scope against the invoked tool. Any failure returns a structured error *and* emits an audit event, which is what turns authentication from a gate into evidence.

## 3. Identity Propagation: How to Flow Human Identity Through Multi-Step Agent Chains

Scope grants capability; identity governs rows. A token that says `orders:read:own` is only meaningful if the data layer knows *whose* “own” is being read. That requires propagating the human identity — intact and verifiable — through every hop of a multi-step agent chain, and terminating it at the database so that row-level security can do its job.

The naive approach, passing a `user_id` as a tool parameter, is not propagation; it is a suggestion the model can rewrite. Identity must travel as **signed, verifiable claims inside the credential**, not as mutable arguments. Four rules make propagation trustworthy.

**Rule one: identity is carried, not re-derived.** Each hop receives the delegation token and its claim chain, never a bare identifier. A sub-agent acting for a user inherits the user’s identity as a claim it cannot forge, and the chain records the hop that introduced it.

**Rule two: delegation narrows, never widens.** A sub-agent’s token must be a subset of its parent’s scopes and TTL. This is monotonic privilege reduction: authority can only shrink as the chain lengthens, so a compromised, over-eager leaf agent’s ceiling is the leaf’s grant, not the root user’s full entitlement. Enforce this at the token-exchange endpoint — refuse any exchange that requests a scope the presenter does not already hold.

**Rule three: the principal is resolved into a database security context.** The MCP server, holding a verified identity, opens the database connection under a role or session variable that the engine enforces. Oracle VPD, MS SQL row-level security, or Postgres RLS then filters every query to the principal’s permitted rows automatically. This is the point where protocol-level authorization becomes data-level authorization, and where a prompt-injected instruction to “read all orders” returns an empty result because the rows simply are not visible to that session.

**Rule four: the chain is auditable end to end.** Every event records `initiating_user` and the full `delegation_chain`, so an investigator can walk backward from a mutated row to the human who set the task in motion. Combined with short TTLs, this gives you what service accounts never could: the ability to answer *who authorized this* without guessing, and to revoke a single user’s authority without touching anyone else’s session.

## 4. Policy Enforcement: Implementing OPA for Tool-Level RBAC

Identity tells you who is acting; scopes tell you the outer envelope of what is possible. Policy is the decision layer that evaluates the *specific* request against the *current* context — and it belongs outside the agent, not inside it. I standardize on **Open Policy Agent (OPA)** as a sidecar to the MCP server, expressing tool-level RBAC as Rego that the server consults before every invocation.

A representative policy evaluates three inputs: the token’s claims (subject, scopes, audience, delegation chain), the requested action (tool name and arguments), and environmental facts (tenant, resource ownership, time, approval state). The output is a decision plus obligations — the conditions the server must satisfy to comply:

“`rego
package mcp.authz

default allow := false

# Refunds require the right scope AND tenant match AND approval above threshold.
allow if {
input.tool == “create_refund”
“refunds:create:under_500” in input.token.scopes
input.arguments.amount <= 500 input.resource.tenant == input.token.tenant } allow if { input.tool == "create_refund" input.arguments.amount > 500
input.approval.granted == true
input.approval.approver != input.token.subject # four-eyes
}
“`

The power of centralizing this in OPA is that policy becomes reviewable, testable, and version-controlled as code — decoupled from tool implementation, so a security architect can tighten a rule without shipping a server release. Two properties matter most for agents.

**Deny by default.** An unrecognized tool, an unknown scope, or a missing input field must fail closed. Agents explore; a permissive default rewards exploration with access.

**Obligations, not just booleans.** A policy can return conditional allow — *permit this refund only if a human checkpoint approves* — which the MCP server enforces as a hard gate before execution. This is how human-in-the-loop control moves from a prompt instruction (“ask before refunding”) into an enforced protocol step the model cannot bypass.

Run OPA co-located with the server, feed it decision inputs, and log every decision with its inputs and the policy revision hash. That last detail is what makes section five possible.

## 5. Audit: Building a Replayable Decision Log for Forensic Security Review

An auditor does not ask what your agent intended. They ask what it did, under whose authority, and whether the record can be trusted. Chat transcripts fail all three tests: they are incomplete, unverifiable, and not reconcilable to the transactions they caused. The corrective is to treat agent runs as **transactions to be journaled**, emitting a structured, versioned, append-only stream of decision events.

The envelope for each event carries a unique `event_id`, a `run_id`, a `parent_event_id`, an `event_type` from a closed vocabulary, a UTC timestamp with sub-second precision, a monotonic `sequence`, and the acting principal with its full delegation chain. Because agent execution is a tree — planning, branching, tool calls, retries, approvals, aborts — a parent-child model preserves the structure that flat logs destroy, letting an investigator walk from the initiating request down to the row that changed.

**Replayability** is the property that elevates this from logging to forensics. To replay a decision you must be able to reconstruct why it was allowed, which means recording not just the outcome but the full decision context: the token claims presented, the tool and its arguments, the resource touched, the OPA policy revision hash, and the obligations satisfied. With those four inputs — claims, action, context, policy revision — you can re-evaluate any historical decision against the policy that was actually live and confirm it should have been allowed, or prove it should not have been.

Three storage disciplines make the log evidentiary. **Immutability:** write events to append-only, WORM-style storage with hash chaining so any edit or reordering is detectable. **Schema-first completeness:** if the schema is defined before the agent ships and non-conforming events cannot be written, completeness becomes a property of the system rather than a matter of developer diligence. **Reconciliation:** every event carries a stable identifier linking an utterance to the database transaction it caused, so an anomalous write traces back to the decision behind it.

The payoff is operational, not just compliance. When an incident responder asks “which runs touched account 4421 last week, and who authorized them?”, a replayable log answers in seconds with verifiable evidence — and the same log is what satisfies your SOC 2 or internal-audit assessor.

## 6. Reference Architecture: A Secure, VPC-Isolated MCP Auth Flow

Binding all of this together, here is the reference topology I deploy for regulated workloads. The flow, end to end:

1. **User → Agent Host.** The user authenticates to the agent host via OAuth 2.1 Authorization Code with PKCE. No implicit grant, no static secret.
2. **Agent Host → Authorization Server (private subnet).** The host requests a task-scoped delegation token via RFC 8693 exchange: narrow scopes, minutes-long TTL, `aud` set to the specific MCP server, and a delegation chain seeded with the user’s identity. All of this happens inside the VPC; the authorization server is never internet-facing.
3. **Agent → MCP Server (private subnet).** Every tool call presents the delegation token over mTLS between the agent runtime and the server. The server validates signature, issuer, audience, expiry, and scope **per request** — never per connection.
4. **MCP Server → OPA sidecar.** The server sends the decision input (claims, tool, arguments, resource, context) to OPA, which returns an allow/deny plus obligations. Deny by default; obligations are enforced as hard gates.
5. **MCP Server → Data Layer.** Holding a verified identity, the server opens a database session under the principal’s security context, so VPD/RLS filters every query to permitted rows. Credentials to the data layer live in a secrets manager and are never exposed to the model.
6. **Every step → Decision Log (write-only).** Authentication results, policy decisions with revision hashes, tool invocations, approvals, and data mutations stream to an append-only, hash-chained store. No path around the log; an event that cannot be written blocks the action.

The network controls matter as much as the identity controls. The MCP server, OPA, the authorization server, and the data layer sit in private subnets with no inbound internet route; only the agent host’s ingress gateway is reachable, through a WAF, and egress from the MCP tier is allowlisted to the data layer. Secrets never leave the secrets manager. The model’s only reachable surface is the MCP tool API — it has no network path to the database, so a successful prompt injection can only request tools that OPA and row-level security will still evaluate.

The net effect: a compromised or steered agent can at worst invoke the narrow, in-scope tools its task token permits, against only the rows its propagating identity is allowed to see, with every decision logged and replayable. That is least privilege expressed at the protocol layer — not as a policy document, but as enforced, auditable mechanism.

—

## Build agentic IAM you can defend in an audit

If your agents are running on shared service accounts, the containment problem is not theoretical — it is a matter of when. OnlineCloud designs and ships MCP-native systems with scoped delegation, OPA policy enforcement, row-level security, and replayable decision logs, deployed inside your VPC and audited end to end. US-based senior engineers, production in 6–10 weeks.

**[Talk to our security architects →](https://www.onlinecloud.dev/)** — free initial consultation, response within 24 hours.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *