Blog

Session isolation is a security boundary for multi-tenant agents

Shared task stores, transports, and identifiers can move data between users even when every request is individually authenticated.

Armor12 min read

Two customers send JSON-RPC requests with the same request ID. Both are authenticated. Both receive successful responses. One response contains the other customer's data.

That is the failure a multi-tenant agent service must make structurally difficult.

Write the invariant before the implementation

An object owned by tenant A must be inaccessible to tenant B unless an explicit, authorized sharing operation says otherwise. Within a tenant, principal and session permissions still determine who can use it.

Apply this to tasks, responses, cancellation, notifications, caches, and conversation resumption. The MCP TypeScript task-store advisory concerned cross-session access to tasks in a shared store. A separate server advisory concerned responses crossing between HTTP clients when instances were shared.

An authenticated HTTP edge cannot repair an ownership field dropped later in the pipeline.

Component Ownership it must retain Failure to look for
Task store Tenant, principal, session, object Fetching by task ID alone
Queue Destination and ownership context Routing a result by request ID alone
Cache The same scope as the underlying data Reusing another user's authorized result
Resume handler Original owner and explicit recovery policy Treating possession of an identifier as permission

Make an unscoped lookup inconvenient

Pass an authenticated context into the data-access layer. Require it when constructing storage and routing keys. The following is illustrative pseudocode:

get_task(
    tenant = authenticated.tenant,
    principal = authenticated.principal,
    session = validated_session,
    task_id = requested_task
)

The tenant and principal come from trusted request context. Accepting those fields from a caller would let the caller choose the authorization scope.

For intentionally shared tasks, make access an explicit policy operation. Shared infrastructure can be efficient without making every object globally addressable.

Test the deployment you actually run

Use two clients against the same process and store. Give them identical request IDs and overlapping task names. Have one create a task while the other lists, reads, cancels, and resumes tasks.

Then introduce disconnects, retries, and delayed notifications. These paths often execute after the original HTTP context has disappeared.

Record a clear expected outcome for each operation. A denied read must not leak object content in its error. A rejected cancellation must not modify the task. A retry must not deliver a stale response to the next connection.

The companion article on cross-session testing turns this ownership model into a concurrent test plan.

mcpmulti-tenantauthorization

See what your own fleet is running

Armor1 discovers and risk-scores every MCP server, skill and tool your developers have connected. Free for personal use, no credit card.

Start Free