Skip to content

Build1 publisher3 min readPublished

Moving MCP secrets into a gateway leaves a hijacked agent holding only a per-run token

Agenthof, an Apache-2.0 Go gateway, holds MCP server credentials so a hijacked agent can steal only a revocable per-run token, a dev.to walkthrough shows. Every tool call crosses the gateway, so it can also refuse ungranted tools and log each call to a hash-chained ledger.

The Engineer · Build desk

Illustration accompanying Moving MCP secrets into a gateway leaves a hijacked agent holding only a per-run token

What happened

  • The common setup puts a live key such as BILLING_API_KEY in the MCP server's env block, one os.environ read from anyone who compromises the agent through prompt injection or a poisoned dependency.
  • On each tool call the gateway injects the real credential just before forwarding upstream and strips it from anything it hands back to the agent.
  • An agent config that omits its tool list is rejected at load, so granting every tool on a server requires typing mode: all explicitly.
  • For OAuth-protected servers, the gateway's broker mints a client-credentials token per call, and the agent never sees that token or the client secret behind it.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure An injected instruction can still drive every granted tool for the life of the run token, so the grant list now sets the ceiling on what a hijacked agent can do.
  • constraint Operators take on the job of labelling every tool as read or write, and the gateway enforces a wrong label exactly as written.
  • cost Concentrating every upstream credential in one gateway process makes that host the single target worth breaching, so it needs tighter protection than the agents it fronts.

The case against the env block starts with how an agent works. It chooses its next action at runtime from text, and some of that text comes from outside: a webpage, a document it summarized, a tool result [3]. The post's author wrote, "You can't fix this by making the agent more careful. The agent is the untrusted part." [4] The direct setup has a second gap. The agent can call any tool the server exposes, and the record of what it called sits in logs it could have influenced [13].

The gateway moves the trust boundary one process over. The credential lives in the gateway's environment. The agent holds a short-lived token scoped to its current run, plus a URL that reaches the MCP server only through the gateway [5]. According to the post, compromising the agent then yields a revocable per-run token in place of the billing key [7]. That token still works until someone revokes it. An attacker who controls the agent can call whatever its grant list allows for the rest of the run. The grant list is where the design has to be strict. In the post's example, an agent named legacy-triage gets every tool on ticket-search but only `get_invoice` and `list_invoices` on billing-mcp [15]. Failing config load when the tool list is missing is the detail I would copy into any gateway [9].

Read-only mode is thinner. `mode: read-only` admits the tools a resource is declared to expose read-only, and the operator's classification decides what counts [10]. A write tool listed in `read_only_tools` passes as read-only. The OAuth example adds a second layer: the broker requests `scope: billing.read` from the identity provider when it mints each per-call token [11]. If the provider and the billing server enforce that scope, a mislabeled write call fails upstream even after the gateway lets it through. If they do not, the label is the only control.

The ledger claim needs one qualification. The post describes a hash-chained, tamper-evident log of every call and every refusal, kept in one place the agent cannot rewrite [12]. Tamper-evident means an edit can be detected. The agent cannot touch the log because it never writes to it. An operator with write access to the gateway host is a different case. In general, a hash chain holds up against that operator only if its latest hash is stored somewhere the operator cannot reach.

The costs are an extra network hop on every tool call and one process that now holds every upstream secret [5]. The post does not include latency figures for that hop. It is candid about the product: "The config is the useful part; the tool is just one way to run it." [14] Agenthof is open source under Apache-2.0 and written in Go [8], so the config semantics can be checked against the code. My context is agents that read outside text and can reach live, write-capable keys. There I think the hop is worth paying for. For an agent holding a sandbox key that can touch nothing billable, the env block is fine.

What to watch

  • Whether Agenthof documents where the audit ledger's latest hash is anchored, which decides whether someone with gateway host access could rewrite the chain.
  • Published latency measurements for the extra gateway hop on MCP tool calls under realistic agent loads.
  • Whether identity providers in real deployments enforce requested scopes like billing.read, which determines whether a mislabeled read-only tool is caught upstream.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories