Securing the ‘Confused Deputy’: Identity Propagation in Agent Chains

Written by

in

![OnlineCloud — Agentic AI & MCP Engineering Studio](https://www.onlinecloud.dev/assets/hero-company-V8Xps1Ae.png)

# Securing the “Confused Deputy”: Identity Propagation in Agent Chains

**By the OnlineCloud security architecture team**

In 1988, Norman Hardy named the confused deputy problem. His compiler — a trusted utility with the file owner’s privileges — could be tricked into overwriting the billing file, not because it was malicious, but because it held *ambient* authority far beyond its caller’s. The deputy never doubted its integrity; it simply could not tell whose authority it was exercising in any given moment.

We have rebuilt that architecture inside our agent chains, and this time the deputy is an LLM. A user says “reconcile last quarter’s invoices,” and one instruction fans out into dozens of tool calls across a database, a CRM, an email system, and a reporting service. The orchestrator authenticates once with a broad service identity, and every downstream tool trusts it unconditionally. A prompt injection buried in one vendor invoice becomes an exfiltration path with database credentials behind it. The blast radius is every system the agent can reach.

As a security architect, I’ve watched this failure mode multiply across client engagements. The fix is not to make agents “safer” in the abstract; it is to make the *identity architecture* honest: propagate the human’s identity through every hop, delegate it with narrowing scope, and audit every link. This post walks through exactly that — OAuth 2.1 scoped delegation, hop-by-hop least privilege, forensic auditability, and a reference architecture built on the Model Context Protocol (MCP).

## 1. The Confused Deputy in Autonomous Multi-Step Tasks

The confused deputy had three ingredients: a privileged program, a less-privileged caller, and a design in which the program’s authority did not depend on the caller’s. Hardy’s insight: authority should travel with the *request*, not sit on the *process*. Capability-based systems were the proposed cure; industry standardized on ambient principals instead — a workable trade-off while the deputy was a compiler running as one user on one machine.

Autonomous agents broke that trade-off in three ways:

**First, one identity now spans many systems.** The orchestrator holds a token (or worse, a long-lived service-account key) that grants access to every tool in the fleet. A “read CRM notes” step that reaches the “delete from Snowflake” endpoint is indistinguishable to the database — the deputy’s authority is the caller’s, full stop.

**Second, the caller is an unreliable narrator.** Prompt injection is the normal adversarial condition of LLM tool loops: any hop that concatenates untrusted content into a prompt can steer subsequent tool calls. One successful injection inherits everything.

**Third, the chain is deep.** Host → orchestrator → planner → tool server → data connector spans four or five trust boundaries; context about the human erodes at each one — unless identity is explicitly carried.

The discipline that fixes all three is the one Hardy pointed at: **never let a deputy act on authority it cannot attribute to a caller.** Every tool invocation must answer “on whose behalf, through what path, and with what scope?” That takes three mechanisms — identity propagation (who), scoped delegation (how authority passes), and least-privilege enforcement at each hop (what can be done) — with audit making all three verifiable.

## 2. Identity Propagation: From Service-Account Auth to User-Scoped Tokens

Service accounts were never a security strategy; they were a *convenience* strategy — one credential, one token refresh, no per-user state. The costs were merely deferred:

– **No attribution.** “The service account deleted the row” is not an answer to “who asked for this?”
– **No granular revocation.** A compromised session revokes the whole agent, not one user.
– **Unbounded blast radius.** The principal is scoped to the *union* of everything the agent might ever need — nobody wants to re-issue credentials when the agent gains a tool.
– **A confused deputy by construction.** The deputy’s authority is fixed and ambient; it is not tied to any caller at all.

The remedy is user-scoped, propagating identity — the “on-behalf-of” pattern behind Kerberos constrained delegation, Windows S4U, and cloud role-chaining. The shape is always the same:

1. The **user authenticates at the host** (OIDC + PKCE), proving *who* they are.
2. The host exchanges that identity for an **access token bound to the orchestration session**, carrying the user as `sub`, the orchestrator as `aud`, and a tight `exp`.
3. Every downstream hop receives a token that **preserves the user’s subject** while naming the current actor — the CRM sees “user U, acting through orchestrator O,” never just “the agent.”

In practice, access tokens should be JWTs (RFC 9068) carrying at minimum `sub` (the human), `iss`/`aud`, `exp`/`iat`, and a unique `jti`. Propagation is not copying one fat token everywhere — it is *re-deriving* a token at each boundary so context (subject, actor, audience, scope) is explicit and verifiable.

Reserve true service identity for genuinely headless work — scheduled batch jobs with no user in the loop — and even then narrow it to the job, mint it short-lived, and log every use. The rule is simple: **ambient authority is a bug; attributed authority is a feature.**

## 3. Scoped Delegation: OAuth 2.1 Token Exchange for Agent Swarms

Propagation gives you a subject that survives the trip; delegation gives you a way to *pass* authority without widening it. For agent swarms, the mechanism is **token exchange**, standardized in RFC 8693 and sitting cleanly on top of OAuth 2.1.

OAuth 2.1 is the consolidation of everything we learned the hard way: PKCE required for public clients, the implicit and password grants gone, per-client redirect URI registration, refresh-token rotation. It matters here because it assumes *short-lived, audience-bound, single-purpose* tokens as the baseline — exactly what a multi-hop agent chain needs.

RFC 8693 adds a new grant type, `urn:ietf:params:oauth:grant-type:token-exchange`, where a client presents a `subject_token` (the authority it holds) and optionally an `actor_token` (who is causing the exchange), and requests a *new* token for a specific `audience` with a specific `scope`:

“`json
POST /token HTTP/1.1
Host: idp.onlinecloud.dev
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token=eyJhbGciOiJSUzI1NiIs…
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&actor_token=eyJhbGciOiJSUzI1NiIs…
&actor_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&audience=https%3A%2F%2Fmcp.crm.internal
&scope=crm%3Aread%3Aorders%20crm%3Aread%3Acustomers
&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
“`

The response is a fresh, short-lived token whose claims encode the delegation: `sub` remains the original user, `act` names the immediate actor, and `aud`/`scope` are **narrowed** to the next tier. A JWT-delegation chain records itself: token *n* carries the actor that minted token *n−1*, so the full path from human to deepest tool is reconstructible from any single token.

For an agent swarm, I enforce five non-negotiable rules on every exchange:

1. **Monotonic narrowing.** An exchange may only reduce scope or keep it equal — never widen. If a request asks for more than it holds, the authorization server denies it. This single invariant stops a compromised mid-tier hop from escalating.
2. **Audience is a hard boundary.** Each token names exactly one audience (an MCP server URI, per RFC 8707 resource indicators). A CRM token presented to the database connector is rejected before any code runs.
3. **Explicit actor.** A chain of `act` claims is the difference between “the user did this” and “the user asked an agent, which asked another agent, which did this.”
4. **Delegation depth limits.** The authorization server stamps each token with a delegation counter and refuses to exchange beyond a configured depth (we run orchestrators at 2–3). This kills recursion and cycles.
5. **Short TTLs, no refresh across boundaries.** Hop tokens live minutes, not hours. A refresh token never crosses a trust boundary; each tier re-authenticates to the policy layer rather than trusting an inherited credential.

## 4. Enforcing Least-Privilege at Each Hop in the Tool Chain

Propagation and delegation make identity *available* at the deepest tier; least privilege makes it *safe* there. A propagated token with unrestricted scope is just a faster confused deputy. The discipline is to treat scope as a property of the *tool call*, not of the *session*.

The MCP authorization model was designed with exactly this in mind: the spec’s authorization flow builds on OAuth 2.1, uses resource indicators so each token is bound to one MCP server’s URI, and supports per-tool authorization decisions at the server. That gives you a concrete enforcement lattice for a typical chain:

| Hop | Audience (MCP server) | Example scope | TTL |
|—|—|—|—|
| Host → Orchestrator | `orchestrator.internal` | `session:plan`, `session:checkpoint` | 15 min |
| Orchestrator → CRM tool | `mcp.crm.internal` | `crm:read:orders`, `crm:read:customers` | 5 min |
| Orchestrator → Fulfillment tool | `mcp.fulfillment.internal` | `fulfill:ship:create` | 5 min |
| CRM tool → Warehouse DB | `pg.warehouse.internal` | `db:select:orders` (row-level filtered) | 5 min |

Note what is *not* in that table: a token that can read the CRM cannot write to it, cannot touch fulfillment data, and the database connector never sees a token wider than `db:select` on orders — even though the human could authorize far more at the host. Each exchange produced a narrower token, so each hop’s worst case is bounded by its own scope.

Three enforcement details matter in practice:

– **Enforce at the server, not the client.** Token introspection before every tool execution, verifying `aud` matches the server’s own URI, `exp` is live, and the requested tool is in scope. Client-side filtering is convenience; server-side checks are security.
– **Push subject claims into the data layer.** This is where OnlineCloud’s row-level access policies pay off: the connector maps token claims (`sub`, group memberships) onto row-level security predicates in Oracle, MS SQL, Postgres, or Snowflake. The database refuses rows the user may not see — the agent cannot retrieve what the user could not, even if it *wants* to.
– **Deny by default.** A tool not explicitly in scope is refused. Scope drift requires a code review and a policy change, never an ad-hoc token with a wider `scope`.

The mental model for every engineer on the chain: **each hop is a deputy with a fixed, caller-derived mandate. If a tool cannot state whose authority it is using and what subset of that authority applies, it should not execute.**

## 5. Audit: Replaying Propagation Logs for Forensic Analysis

Least privilege limits the damage a confused deputy can do; audit ensures you can find out what happened when one does. The requirement is not “we log tokens” — it is **forensic replayability**: the ability to take any observed tool call and reconstruct the full delegation path that produced it.

That demands a propagation log at every exchange point. Each event should carry:

“`json
{
“event”: “token_exchange”,
“ts”: “2026-09-06T14:32:07.418Z”,
“trace_id”: “3f9c1a…”,
“parent_jti”: “a1b2…”,
“issued_jti”: “c3d4…”,
“subject”: “user:uid-4471”,
“actor_chain”: [“orchestrator:ord-882”, “planner:agent-12”],
“audience”: “mcp.crm.internal”,
“granted_scope”: [“crm:read:orders”],
“requested_scope”: [“crm:read:orders”, “crm:write:orders”],
“decision”: “narrowed”,
“exp”: 300
}
“`

Linked by `jti` (parent → issued) and a `trace_id` surviving across HTTP hops, these events form a tree you can walk in either direction. Replaying a log means answering four questions:

1. **Who authorized this?** Walk the `actor_chain` to the root `subject`.
2. **Through what path?** Rebuild the token lineage — via the sanctioned orchestrator, or via a sideways delegation from a sibling agent?
3. **Was scope honored?** Every exchange records requested vs. granted scope. A run of “narrowed” decisions is healthy; a single “widened” decision is an incident.
4. **Was anything replayed?** `jti` + `aud` + `exp` reveal the same token presented to two audiences, or after its window closed. Replay is the signature of a stolen token — and it should page someone.

For the logs to be trustworthy evidence, apply tamper-evident practices: append-only storage, hash-chaining each record to its predecessor, and WORM retention that outlives your token TTLs by months. Because an agent chain spans services, ship every propagation event to a single audit bus with the shared `trace_id` — per-service logs that cannot be correlated are, forensically, not logs at all. This is the “audited end-to-end” bar we hold every MCP deployment to: if you cannot replay the delegation path behind a given invoice read on demand, the identity architecture is unfinished.

## 6. Reference Architecture: Identity Flow from Host to Deep-Tier MCP Tools

Putting it together, this is the identity flow we ship for production agent chains — user at the top, row-level-secured data connectors at the bottom, every boundary enforcing the rules above:

“`
┌──────────────────────────────────────────────────────────────────┐
│ HOST (chat / IDE / portal) user authenticates (OIDC+PKCE) │
└───────────────┬──────────────────────────────────────────────────┘
│ identity token → exchange #1 (aud=orchestrator)
┌───────────────▼──────────────────────────────────────────────────┐
│ AUTHORIZATION SERVER / POLICY LAYER (RFC 8693 exchange, │
│ introspection, delegation-depth, scope-narrowing policy) │
└───────┬──────────────────┬───────────────────────┬───────────────┘
│ exchange #2 │ exchange #2 │ exchange #2
┌───────▼────────┐ ┌──────▼─────────┐ ┌───────▼──────────────┐
│ ORCHESTRATOR │ │ PLANNER / │ │ MCP GATEWAY │
│ agent (depth1) │ │ sub-agents │ │ (broker, SSE/HTTP) │
└───────┬────────┘ └──────┬─────────┘ └───────┬──────────────┘
│ exchange #3 (aud=MCP server URI, narrow scope)
┌───────▼──────────────────────────────────────────▼──────────────┐
│ TIER-1 MCP SERVERS per-tool authz, introspection, rate limits│
└───────┬───────────────────────────────┬─────────────────────────┘
│ exchange #4 (aud=connector) │ exchange #4
┌───────▼──────────────┐ ┌───────▼─────────────────────────┐
│ CRM / ERP connectors │ │ DATA CONNECTORS: Oracle, MS SQL,│
│ (user-scoped tokens) │ │ Postgres, Snowflake — row-level │
└──────────────────────┘ │ security from propagated claims │
└─────────────────────────────────┘
every hop → AUDIT BUS (jti-linked propagation events, trace_id)
“`

Walking the flow: the **host** authenticates the human and holds the only long-lived session. The **authorization server** is the single place that understands delegation policy — it narrows scope, binds audience, checks depth, records every exchange. The **orchestrator** never sees the user’s host token; it receives a derived token naming the user as `sub` and itself as `aud`. When it invokes a tool, the **MCP gateway** brokers the call over streamable HTTP or SSE; the **tier-1 MCP server** introspects the presented token, verifies audience and tool scope, then enforces its own per-tool policy. Deep connectors — Oracle, MS SQL, Postgres, Snowflake — receive narrower tokens still and map propagated claims onto row-level security predicates. If any single hop is compromised — an injected prompt, a buggy tool, a stolen token — the damage is bounded by that hop’s scope, the subject remains attributable to a human, and the full path is reconstructible from the audit bus.

Two deployment notes. First, the authorization server’s policy tables are the *real* product: scope catalogs per tool, delegation-depth ceilings, and audience allow-lists should be versioned and reviewed like firewall rules. Second, make failure loud: exchange denials, scope-narrowing decisions, and replay detections should be observable metrics — a confused deputy that fails silently is one you will meet again in the incident report.

## Build This Into Your Agent Stack

Identity propagation is not a compliance checkbox; it is the difference between an agent that can safely touch production data and one that *is* a production incident waiting for a prompt injection. At OnlineCloud, we design MCP servers around real auth boundaries — stateless servers with exchange-based auth, rate limits, and jti-linked audit logs out of the box, wired to Oracle, MS SQL, Postgres, Snowflake, and the CRMs your team actually uses. We ship evaluated, observable agent systems in 6–10 weeks, not quarters — and every one can answer “whose authority ran this tool call, and why?”

If you are wiring autonomous agents to real business data, let’s talk about the identity architecture before the agents do.

**[Contact OnlineCloud](https://www.onlinecloud.dev/)** — free initial consultation, US-based senior engineers, response within 24 hours. Or email us directly at **contact@onlinecloud.dev**.

Comments

Leave a Reply

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