The Containment Era is here. →Explore

TL;DR

  • Every Model Context Protocol (MCP) server in your cluster is an active egress path. Without network-layer enforcement, a rogue or compromised server can reach anything the host can reach.

  • The Aviatrix-Obot Validated Containment Architecture (VCA) enforces a per-server, default-deny egress policy at the spoke gateway using Kubernetes-native Custom Resource Definitions (CRDs). No sidecars. No code changes.

  • Obot is the deployment plane: it spawns MCP servers with predictable labels. Aviatrix is the enforcement plane: it targets those labels and drops disallowed egress inline.

  • Every blocked or permitted connection ties back to a specific MCP server, a specific destination FQDN, and a specific timestamp. That is the forensic proof regulated industries require.

Introduction

An MCP server is a privileged process running inside your cluster with access to credentials, data APIs, and external network paths. Most enterprises deploying AI agents have dozens of them. Few have an accurate inventory. Almost none have per-server egress controls. This is not a gap so much as a founding principle.

The threat is already a reality. In March 2026, supply-chain attacks compromised npm packages used by Axios, LiteLLM, Trivy, and others. A poisoned dependency inside an MCP server inherits the server’s egress permissions for free. The server is permitted to call your Electronic Health Record (EHR). The malicious package inside it calls an attacker-controlled endpoint instead. Your Kubernetes NetworkPolicy does not catch it: NetworkPolicy operates at the IP and port layer, and the attacker’s endpoint listens on port 443 just like the legitimate one.

One control stops this: network-layer, domain-aware egress enforcement scoped per MCP server. That is what this architecture delivers.

Introducing the Validated Containment Architecture for Obot MCP Servers

This VCA pairs two capabilities:

Obot (deployment plane): Obot deploys MCP servers on Kubernetes with consistent, predictable labels. Every pod gets an app=<server-id> label that uniquely identifies it. That label is the contract between the deployment plane and the security plane.

Aviatrix Distributed Cloud Firewall (enforcement plane): Aviatrix DCF enforces egress policy at the spoke gateway using Kubernetes-native FirewallPolicy CRDs. Policy evaluation runs at the eBPF dataplane inline, before packets leave the cluster. No sidecars, no Istio, no per-pod resource overhead.

When you deploy an MCP server in Obot and set egressDomains on the server manifest, the built-in network policy controller automatically generates a FirewallPolicy CRD and pushes it to the Aviatrix spoke gateway. No operator steps. The policy is live before the first request leaves the pod. This shipped in Obot v0.21.0.

Lab Notes

Environment

The reference lab runs on Azure Kubernetes Services (AKS) with Aviatrix 8.2. The architecture applies equally to Amazon Elastic Kubernetes Service (EKS) and Google Kubernetes Engine (GKE), with cloud-specific infra domain lists documented in the reference architecture.

Two namespaces, two responsibilities:

Namespace

Contents

Purpose

obot-system

Obot orchestrator, Aviatrix network policy controller

Deployment and policy plane

obot-mcp

MCP server pods, traffic generator

Workload enforcement target

DCF enforcement targets obot-mcp. Obot’s own egress (Anthropic API, GitHub, Helm chart repo) is permitted separately via a V1 rule scoped to obot-system pod IPs.

Legs 1 and 2 are intrinsic to MCP server design. This architecture breaks Leg 3 at the network layer.

Three-Tier Policy Evaluation

Traffic from any MCP server pod traverses three tiers in order.

Tier 1: V1 named permit rules evaluate first. These cover infrastructure egress (ACR, blob storage, AKS API server) and Obot orchestrator egress. They are CIDR-scoped and exist independent of any MCP workload.

Tier 2: FirewallPolicy CRDs are per-pod permit/deny rules applied by the Aviatrix network policy controller. Each MCP server carries its own CRD in obot-mcp, specifying exactly which domains it may reach on port 443.

Tier 3: POST_RULES default action catches everything else. Set to DENY + LOG. Any connection not explicitly permitted in Tier 1 or Tier 2 is blocked and logged.

Why the order matters: a V1 deny at any priority evaluates before the CRD block, which would negate all CRD permits. The POST_RULES default action must be a default action rule, not a V1 entry.

Three-Tier Policy Evaluation

The Label Contract

After Obot deploys an MCP server, the pod gets a deterministic label:

kubectl get pods -n obot-mcp -l "mcp-user-id" \ 

  -o custom-columns="NAME:.metadata.name,APP:.metadata.labels.app,IP:.status.podIP" 

NAME              APP        IP 

ms1kbsng-xyz      ms1kbsng   10.244.0.47 

pt5rfqaz-abc      pt5rfqaz   10.244.0.51

The app label is the Obot server ID. Aviatrix targets this label in the FirewallPolicy CRD. No knowledge of Obot internals required on the Aviatrix side.

Deployment plane and security plane share a single stable identifier: the pod label, and nothing else. That is the whole contract.

FirewallPolicy CRD: One Per MCP Server

A minimal FirewallPolicy for a benign MCP server that needs to reach api.openai.com:

apiVersion: networking.aviatrix.com/v1alpha1 

kind: FirewallPolicy 

metadata: 

  name: claims-lookup-fw 

  namespace: obot-mcp 

spec: 

  smartGroups: 

    - name: claims-lookup-pods 

      selectors: 

        - type: "k8s" 

          k8sNamespace: "obot-mcp" 

          tags: 

            app: "ms1kbsng" 

    - name: any-destination 

      selectors: 

        - cidr: "0.0.0.0/0" 

  webGroups: 

    - name: approved-egress 

      domains: 

        - "api.openai.com" 

  rules: 

    - name: allow-openai 

      action: permit 

      selector: 

        matchLabels: 

          app: ms1kbsng 

      destinationSmartGroups: 

        - name: any-destination 

      webGroups: 

        - name: approved-egress 

      protocol: tcp 

      port: 443 

      logging: true 

    - name: deny-all 

      action: deny 

      selector: 

        matchLabels: 

          app: ms1kbsng 

      destinationSmartGroups: 

        - name: any-destination 

      protocol: any 

      logging: true

The deny-all rule at the end is not redundant with POST_RULES. It produces per-server log lines with the CRD name in the Rule column, which is exactly what incident response needs: which server tried to reach which destination, and which policy blocked it. A rogue MCP server with no legitimate external egress gets an even simpler CRD: no permit rules, deny-all only.

What Discovery Looks Like Before Enforcement

Before any FirewallPolicy CRDs are applied, set the DCF default action to PERMIT with logging. After a few minutes of traffic, CoPilot Policy Logs shows:

  1. benign-mcp pod IP calling api.openai.com on port 443: Permitted (Default Action Rule)

  2. rogue-mcp pod IP calling httpbin.org on port 443: Permitted (Default Action Rule)

  3. rogue-mcp pod IP calling webhook.site on port 443: Permitted (Default Action Rule)

Both servers are visible, but neither is contained. This is, remarkably, considered a mature posture. [Screenshot: CoPilot Policy Logs, pre-enforcement — mixed benign and rogue traffic both Permitted under Default Action Rule]

What Enforcement Looks Like After CRDs Apply

Apply the CRDs first, wait for reconciliation (the controller sets status.ruleset when the policy reaches the gateway), then flip the default action to DENY:

kubectl apply -f k8s/benign-mcp-fw.yaml -f k8s/rogue-mcp-fw.yaml 

# Wait for reconciliation 

until kubectl get firewallpolicy rogue-mcp-fw -n obot-mcp \ 

  -o jsonpath='{.status.ruleset}' 2>/dev/null | grep -q '.'; do sleep 5; done

kubectl apply -f k8s/benign-mcp-fw.yaml -f k8s/rogue-mcp-fw.yaml # Wait for reconciliation until kubectl get firewallpolicy rogue-mcp-fw -n obot-mcp \ -o jsonpath='{.status.ruleset}' 2>/dev/null | grep -q '.'; do sleep 5; done

CoPilot Policy Logs now shows:

  1. benign-mcp calling api.openai.com: Rule = allow-openai, Result = Permitted

  2. rogue-mcp calling httpbin.org: Rule = deny-all, Result = Denied

  3. rogue-mcp calling webhook.site: Rule = deny-all, Result = Denied

One FirewallPolicy per MCP server. Default-deny breaks the exfiltration chain. The benign server keeps its single approved dependency. The rogue server has nowhere to go.

CoPilot Policy Logs, post-enforcement — benign Permitted under allow-openai, rogue Denied under deny-all

[Screenshot: CoPilot Policy Logs, post-enforcement — benign Permitted under allow-openai, rogue Denied under deny-all]

The Compliance Audit Record

Every row in Policy Logs carries:

Column

Value

Why it matters

Source IP

Pre-NAT pod IP

Ties back to specific MCP server, not translated gateway IP

SNI

Destination FQDN

Exact domain, not just a port or IP

Rule

CRD rule name

Which policy fired: allow-openai, deny-all, or Default Action Rule

Result

Permitted / Denied

Binary enforcement outcome

Timestamp

Event time

Lines up with incident timeline

To translate a pod IP to a server name during an incident:

VCA for Obot 4 - Translate Pod IP to a server name

kubectl get pods -n obot-mcp -o wide

Follow the chain: pod IP to pod name to Obot server ID to the user who deployed it. For HIPAA, PCI-DSS, and SOC 2, this is architectural proof of enforcement, not a policy document.

How Obot Generates Enforcement Policy

The egressDomains field on each MCP server manifest drives the FirewallPolicy. Set it via the Obot API or UI and the built-in network policy controller reconciles the corresponding CRD in obot-mcp:

VCA for Obot 5 - How Obot Generates Enforcement Policy

{ 

  "name": "claims-lookup", 

  "npxConfig": { 

    "package": "@org/claims-lookup-mcp", 

    "egressDomains": ["api.openai.com"] 

  } 

}

The controller produces a FirewallPolicy CRD with the exact structure shown above. Delete the MCP server in Obot and the controller garbage-collects the policy at the gateway. No orphaned permits accumulate.

Deploy a server in Obot. Get a network-layer containment policy at the gateway.

Conclusion

MCP servers are a new egress surface, and the default posture for almost every enterprise running them is wide open. The Aviatrix-Obot Validated Containment Architecture changes that posture at the architecture level: one policy per server, enforced inline at the spoke gateway, with a complete audit record tied to workload identity.

Obot deploys. Aviatrix contains. Every connection is logged, named, and attributable.

Schedule a Demo

See this architecture in your environment. We will identify the MCP servers running today, walk through the FirewallPolicy CRD model, and show you the egress paths you do not currently see.

Learn about other Aviatrix Validated Containment Architectures.

Explore Zero Trust for AI Workloads and Aviatrix AgentGuard, which enforce security polices for AI workloads at the network layer.

Frequently Asked Questions

AWS handles the AgentCore service infrastructure under the shared-responsibility model. The agent itself — the code running inside the Runtime container — initiates its own outbound traffic. By default, that egress routes to the open internet through AWS-managed infrastructure with no in-line inspection point a customer's security team can reason about. Compromised agent traffic is indistinguishable on the wire from legitimate calls. The VCA is the enforcement point.

Not for the baseline network controls. The Runtime is created in VPC mode with a flag at creation time. AgentCore drops the per-session microVM's ENI in subnets you control; egress follows the subnet's route table from there. The agent code does not change. Selective TLS decryption on supply-chain hosts is the one optional dependency that requires the Aviatrix MITM CA in the container's trust store — a five-line addition to the Dockerfile, with a fetch script provided in the repo.

25 to 30 minutes on a fresh AWS account with one terraform apply. That includes the Aviatrix transit, the AgentCore spoke, the client spoke, the two interface VPC endpoints, the Distributed Cloud Firewall SmartGroups, WebGroups, and policies, the IAM guardrail, the ECR repo with the sample agent image, and a sample AgentCore Runtime you can invoke immediately for smoke tests. Destroy is one command and leaves zero orphans.

IAM condition keys on bedrock-agentcore:subnets and bedrock-agentcore:securityGroups. The VCA's IAM managed policy denies CreateAgentRuntime, CreateAgentRuntimeEndpoint, and UpdateAgentRuntime unless the request's subnets and security groups come from the approved set. A PUBLIC mode Runtime or a foreign-subnet attachment fails at the AWS API with an explicit deny — the call never reaches AgentCore. The drift scenario in the blueprint UI demonstrates this end-to-end.

This release covers AgentCore Runtime. Browser, Code Interpreter, and Gateway are deferred to a follow-on. The architecture accommodates them — the MITM CA is already provisioned to Secrets Manager so Browser and Code Interpreter sessions can pull it on session start via the platform's first-class certificates parameter — but they are not in the current deployable scope.

The architecture is one repository, one Terraform module, one set of validated controls. Everything required to deploy on a fresh AWS account is in the box. You’ll receive an insertion pattern, SmartGroup model, Baseline Distributed Cloud Firewall policy pack, Bill of Materials, and GitHub repository.

Secure The Connections Between Your Clouds and Cloud Workloads

Leverage a security fabric to meet compliance and reduce cost, risk, and complexity.

Cta pattren Image