Forty percent of the MCP servers reachable from the public internet ask for nothing at all. A 2026 measurement of 7,973 internet-reachable remote MCP servers found 40.55% exposed their tools with no authentication — no token, no identity, just a tools/call endpoint waiting for whoever shows up. And the agents on the other end are not hard to fool: across 45 live servers and 353 tools, tool-poisoning attacks succeeded against flagship reasoning models at rates up to 72.8%. The threat is not theoretical anymore either. The OWASP Top 10 for Agentic Applications (2026 edition) documents the first in-the-wild malicious MCP server — an npm package impersonating the legitimate Postmark email service that secretly BCC'd every outgoing email to an attacker-controlled address.
So here is the uncomfortable shape of the default setup: a probabilistic system that can be talked into anything, holding long-lived credentials, talking to tool servers that mostly don't check who's calling. Every incident in this genre has the same root cause. The model was the enforcement point, and the model is the one component in the stack that cannot say no reliably.
Red Hat's January 2026 guidance on building effective AI agents with MCP lands on the fix: put a gateway between the agent and everything it can touch. Agents never hold infrastructure credentials directly. Every tool call is validated and authorized against policy-as-code, then executed in a short-lived isolated environment. The gateway — not the model — is the enforcement point. This post turns that guidance into a concrete blueprint: the request path, the credential layer, an actual Rego policy, the sandbox lifecycle, what a PaaS gateway should enforce, and where this pattern beats both raw-credential agents and the network-gating alternatives.
The pattern in one picture
Every action an agent takes flows through five stages, and the agent controls exactly one of them:
agent ──▶ gateway ──▶ policy decision ──▶ scoped credential ──▶ ephemeral runner ──▶ audit log
│ │ │ │
validate the OPA: allow, deny, mint a token that execute, then
call shape or require can only do this destroy the
(schema, approval one thing environment
schema-only)The agent submits intent ("call deploy.create with these arguments"). The gateway validates the call shape against the tool's contract — Red Hat's guidance puts it well: effective MCP work starts with the contract, and tool schemas should say exactly what the model can rely on and nothing more. Then Open Policy Agent decides. Only on allow does the gateway mint a credential, and that credential is scoped to the single approved action. Execution happens in an environment that ceases to exist when the call finishes. Every stage writes to an audit log keyed to the agent session.
Note what this buys before any policy is even written: a poisoned tool description can still trick the model into requesting something stupid, but the request dies at the policy decision unless it was already permitted. Prompt injection moves from "game over" to "a denied log line."
Layer 1 — the agent never holds credentials
The highest-leverage rule is also the simplest: nothing the agent can read, print, or leak should be a credential that outlives the call. Red Hat's own AgentOps reference architecture (the claude-code-openshift ADR on MCP gateway tool governance) states it as credential isolation — MCP server credentials live in the gateway's secret store and are never mounted into the agent pod — paired with token exchange per RFC 8693, where a broad access token is swapped for a narrowly-scoped one per backend server.
In practice the layer has three moves:
- Exchange, don't forward. The agent authenticates to the gateway once (its session identity). For each approved downstream call, the gateway mints a token scoped to that tool, that argument set, and a short expiry — minutes, not days. A leaked token buys an attacker one already-executed action, not a standing account.
- Keep the real secrets server-side. API keys, cloud credentials, and service tokens live in the gateway's vault. The agent's context window — which is exfiltratable by design, since its contents flow to the model provider — never contains them.
- Scope by verb class, not just by service. A token that can read deployment status and a token that can delete the deployment are different tokens. This is the distinction MCP's own authorization story keeps missing: without a governed layer, MCP defaults to shared credentials and broad scopes, as CData's 2026 field-level-security analysis of 1,899 open-source servers confirmed (7.2% carried general vulnerabilities, 5.5% showed MCP-specific tool poisoning).
If you implement one layer this quarter, implement this one. Credential isolation alone converts every prompt-injection success from infrastructure compromise into a denied request.
Layer 2 — every tool call faces policy-as-code
The policy decision is where Open Policy Agent earns its place. OPA is a general-purpose policy engine: you write allow/deny rules in Rego, and the gateway asks it about every call before anything runs. The community has already converged on the shape — a default allow := false root, a read-verb allowlist layered under per-resource rules, and fail-closed behavior when OPA itself is unreachable. Below is a minimal but complete starting policy for an infrastructure gateway:
package infra.agent
import rego.v1
# Default deny: a tool call is forbidden unless a rule below allows it.
default allow := false
# Reads are cheap and reversible: any authenticated session may list and describe.
allow if {
input.tool.verb in {"get", "list", "describe", "logs"}
input.session.authenticated
}
# Mutating verbs need three things: a fresh approval, a scope match,
# and dry-run-first for anything destructive.
allow if {
input.tool.verb in {"create", "update", "restart", "deploy"}
input.approval.fresh
input.token.scopes[_] == input.tool.required_scope
not input.tool.destructive
}
allow if {
input.tool.destructive
input.approval.fresh
input.approval.human
input.tool.dry_run_completed
input.token.scopes[_] == input.tool.required_scope
}
# If OPA is down, nothing runs. Fail closed, never open.Four properties make this more than a firewall rule. First, default deny means a brand-new tool with no policy is unreachable until someone writes one — the opposite of MCP's default-open posture. Second, the read/mutate split matches how operators actually think: Canopii's State of MCP Security 2026 counts 307 servers with destructive scopes and 405 with sensitive-data access sitting behind the same flat credential as list. Third, dry-run-first for destructive verbs turns "the agent deleted production" into "the agent produced a plan a human approved." Fourth, fail-closed closes the meta-hole: an attacker who can take OPA down should get silence, not a permissive fallback.
Policies are code, so treat them like code: version them, review them, test the deny cases. The most valuable test in the suite is the one that asserts the new dangerous tool is denied by default.
Layer 3 — execution happens in short-lived sandboxes
Authorization decides whether a call runs. The sandbox decides what a permitted call can damage. Even an allowed tool can misbehave — a build script with a compromised dependency, a migration that touches more rows than the plan said. So the gateway executes approved actions inside ephemeral runners: isolated environments created per call and destroyed afterward.
| Lifecycle stage | What happens | What survives |
|---|---|---|
| Provision | Fresh microVM or container from a pinned image, no ambient credentials, network egress allowlisted per tool | Nothing yet |
| Inject | Gateway mounts only the scoped token and the approved arguments | The audit record of what was injected |
| Execute | Tool body runs with a wall-clock timeout; stdout/stderr streamed to the log | Output returned through the gateway, never directly to the agent's network |
| Destroy | Filesystem, processes, and temporary credentials wiped on success, failure, or timeout | The audit log entry: who asked, what policy version decided, what ran, what it returned |
Two rules keep this honest. Anything stateful the workflow needs — the plan file, the migration checkpoint — lives in gateway-managed storage and is re-attached explicitly, never left on a warm box "for convenience." And the agent's own long-running process is never an execution venue: no bash tool with the platform's environment variables, no shared working directory with standing keys. CNCF's August 2026 shadow-AI threat-modeling post lists sandboxing (kagent is the named project) as its own column in agent governance for exactly this reason — policy without isolation is a bouncer with no walls.
What a PaaS gateway should enforce
Map the pattern onto a platform whose API is already machine-readable by design — a Render-compatible REST/GraphQL surface with an MCP server in front of it, the shape bex documents at api.bex.co/mcp with OAuth 2.1, short-lived bearer tokens exchanged from API keys, and three scopes (bex.read, bex.write, bex.sensitive) that already separate reads from mutations. The gateway layer above that surface enforces five things:
| Enforcement | Concrete rule | Why it matters |
|---|---|---|
| Per-session scoped tokens | One token per agent session, minted at session start, carrying only the scopes the task needs; a deploy task gets bex.write, a status check gets bex.read | A token scoped to reads cannot become a deletion, however confused the model gets |
| Mutating-verb policy | create/update/restart/deploy require a fresh approval record; destructive verbs additionally require human approval plus a completed dry run | Mirrors the Rego above; the 403-a-tool-call-returns-when-the-token-lacks-scope behavior becomes policy, not accident |
| Dry-run-first defaults | Destructive calls execute a plan phase whose output is shown before the execute phase is even authorized | Converts irreversible actions into two-step actions with a human-shaped gap |
| Minimal tool surface | The gateway exposes only the tools the session needs, not the platform's full catalog; new tools default to denied | Shrinks the tool-poisoning target from "every description the model read" to a handful of reviewed schemas |
| Every call audited | Session id, tool, arguments, policy version, decision, token id, output hash — appended per call | When something goes wrong, the question "what did the agent do?" has an answer that isn't the agent's own recollection |
None of this replaces the platform's own auth — OAuth discovery, token expiry, scope checks at the API remain. The gateway is the layer that makes those primitives agent-shaped: short-lived, least-privilege, and default-deny by construction rather than by careful per-integration configuration.
Gateway vs raw credentials vs gating the network
Two product-shaped alternatives deserve an honest comparison, because they solve adjacent problems. agentgateway (Solo.io's Rust data plane, now the AI data plane under kgateway and governed in the open) gives you MCP auth and a tool-call witness at the traffic layer. Tailscale's Aperture (open alpha February 2026) puts identity-linked governance in front of AI traffic on the tailnet: which agent, which human, which MCP server, with spend metering and audit. Both are real and useful. Neither is the pattern above — they gate the network path, not the action.
| Raw-credential agent | Network-gating (Aperture-style) | Action-gating gateway (this pattern) | |
|---|---|---|---|
| What is gated | Nothing: the model holds keys | Who can reach which server, with what identity | Whether this specific call, with these arguments, may run |
| Credential exposure | Long-lived keys in the context window | Keys replaced by network identity — better, but a permitted agent still wields full tool power | Agent holds no infrastructure credentials at all |
| Poisoned tool description | Likely compromise | Still reaches the model; a permitted path stays permitted | Request dies at policy unless pre-authorized |
| Blast radius of one bad call | The whole account | Everything the network identity may reach | One scoped action in a disposable sandbox |
| Auditability | Whatever the agent reports | Connection- and server-level logs | Per-call: intent, policy version, decision, output hash |
| Cost | Zero to set up; unbounded downside | Tailnet/identity rollout | A gateway service and a Rego file to maintain |
The rows compose rather than compete: run the gateway behind network identity, and witness tool calls at both layers. But if you only build one, build action-gating. A network gate answers "may this agent talk to the deploy server?" The incidents that hurt answer a narrower question — "may it run this delete, now, with these arguments?" — and only the gateway sees that question at all.
Start with the credential, end with the audit
Adoption order matters, because each layer pays for the next. First, pull credentials out of agent reach: session tokens, vault-held secrets, per-call exchange. That single move downgrades prompt injection from breach to nuisance. Second, write the ten-line default-deny policy and the read/mutate split; add dry-run-first the first time a destructive verb enters the catalog. Third, move execution into ephemeral runners so allowed-but-wrong calls still detonate inside a box you already planned to destroy. Fourth, turn on per-call audit and read it after every incident — the log is the only witness that doesn't confabulate.
The direction of travel is clear. Red Hat is productizing the MCP gateway (Connectivity Link 1.4 carries one as a Technology Preview, with MCP-as-a-Service as the stated longer-term vision), Solo is building the open data plane, Tailscale is wiring identity to it, and CNCF's threat-modeling work treats agent governance as a first-class column rather than a footnote. The platforms that survive the agent era won't be the ones with the cleverest system prompts. They'll be the ones where the model was never trusted in the first place — because the gateway was.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Its MCP server at api.bex.co/mcp speaks OAuth 2.1 with short-lived, scope-separated tokens (bex.read, bex.write, bex.sensitive), which is the credential layer this pattern starts from. Star the repo on GitHub or deploy your first app today.



