> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aviatrix.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configure MCP Server Egress Containment with Obot on AKS

> Step-by-step instructions for enabling Aviatrix DCF egress containment for MCP servers managed by Obot on Azure Kubernetes Service: IP masquerade configuration, cluster onboarding, DCF policy setup, and per-server domain declaration.

When complete, every MCP server Obot deploys on AKS will automatically receive a
workload-scoped containment policy. Egress domain declarations are managed
through Obot's server catalog. The Aviatrix network policy controller translates
those declarations into `FirewallPolicy` CRDs, enforced at the VNet boundary by
the Aviatrix spoke gateway. When a server is removed, its policy is removed with
it.

For the business case and architecture overview, see <a href={"/docs/enterprise/" + "10.1" +
"/solutions/security-for-ai/mcp-server-egress-security"}>MCP Server Security
Through Containment Architecture</a>. For the complete Terraform reference
implementation, see the
[obot-mcp-egress-azure blueprint](https://github.com/AviatrixSystems/aviatrix-blueprints/tree/main/blueprints/obot-mcp-egress-azure).

<img src="https://mintcdn.com/aviatrix-14b37c43/SoyeMF4MpIwhhamV/images/guides/security/obot-enforcement-architecture-technical.png?fit=max&auto=format&n=SoyeMF4MpIwhhamV&q=85&s=e2f163b359a370eb6549ab4b6158a6a3" alt="Reference architecture: namespace layout, NPC controller, FirewallPolicy CRDs, and Aviatrix three-tier enforcement" width="2382" height="3369" data-path="images/guides/security/obot-enforcement-architecture-technical.png" />

## Prerequisites

* Aviatrix Controller 8.2 or later
* CoPilot 4.31 or later
* <a href={"/docs/enterprise/" + "10.1" + "/guides/security/dcf/dcf-enable-feature"}>DCF for Kubernetes enabled</a>
  with **Log Enrichment** turned on
* A <a href={"/docs/enterprise/" + "10.1" +
  "/guides/platform-administration/cloud-account-create"}>cloud account
  onboarded in CoPilot</a> for the Azure subscription hosting the AKS cluster
* [Obot](https://obot.ai) 0.21.0 or later (earlier versions do not include
  MCPNetworkPolicy support)

## Procedure

<Tabs>
  <Tab title="CoPilot UI">
    <Steps>
      <Step title="Deploy an Aviatrix spoke gateway in the cluster VPC">
        An Aviatrix spoke gateway in the same VPC as your Kubernetes cluster is the
        enforcement point where the Aviatrix controller applies containment policy to
        MCP server egress traffic.

        1. In CoPilot, navigate to **Cloud Fabric > Gateways > Spoke Gateways** and
           click **+ Spoke Gateway**.

        2. Provide the following details:

           | Parameter         | Value                                                                           |
           | ----------------- | ------------------------------------------------------------------------------- |
           | **Name**          | A name for the gateway, for example `mcp-containment-spoke`                     |
           | **Cloud**         | The cloud provider hosting the Kubernetes cluster                               |
           | **Account**       | The cloud account for the cluster                                               |
           | **Region**        | The region where the cluster runs                                               |
           | **VPC/VNet**      | The VPC or VNet where the cluster nodes run — this must match the cluster's VPC |
           | **Instance Size** | Select an instance size appropriate for your environment                        |

        3. In the **Instances** section, select a subnet in the cluster VPC for the
           gateway instance.

        4. Click **Save**.

        Wait for the gateway status to show **Up** before continuing. You can monitor
        progress at **CoPilot > Monitor > Notifications > Tasks**.
      </Step>

      <Step title="Confirm network topology">
        The spoke gateway must be deployed in the **gateway subnet**, with the AKS
        cluster in the **node subnet** of the same VNet. Pod egress routes to the
        gateway's public IP via the node subnet's default route. Confirm your subnet
        CIDRs and gateway placement before continuing.

        <img src="https://mintcdn.com/aviatrix-14b37c43/SoyeMF4MpIwhhamV/images/guides/security/obot-mcp-egress-azure-topology.svg?fit=max&auto=format&n=SoyeMF4MpIwhhamV&q=85&s=5aed8f203ded46fadf5c05629ff04a18" alt="AKS VNet topology: node subnet with AKS cluster, gateway subnet with Aviatrix spoke gateway, egress routing path" width="1200" height="560" data-path="images/guides/security/obot-mcp-egress-azure-topology.svg" />
      </Step>

      <Step title="Disable Azure IP masquerade">
        Azure's `ip-masq-agent` rewrites pod source IPs to node IPs before outbound
        traffic reaches the spoke gateway. Aviatrix SmartGroups resolve to pod IPs, so
        masqueraded traffic does not match Kubernetes label selectors and FirewallPolicy
        rules have no effect.

        Apply the following ConfigMap to disable SNAT for all external destinations:

        ```bash theme={null}
        kubectl apply -f - <<EOF
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: azure-ip-masq-agent-config
          namespace: kube-system
          labels:
            component: ip-masq-agent
            kubernetes.io/cluster-service: "true"
            addonmanager.kubernetes.io/mode: EnsureExists
        data:
          ip-masq-agent: |
            nonMasqueradeCIDRs:
              - "0.0.0.0/0"
        EOF
        ```

        <Warning>
          Apply this ConfigMap before deploying Obot or enabling containment policies.
          Pods already running when this change is applied will continue to use node IPs
          until they are restarted.
        </Warning>
      </Step>

      <Step title="Install the Aviatrix Kubernetes Firewall Helm chart">
        The Aviatrix Kubernetes Firewall Helm chart deploys the in-cluster components
        that allow the Aviatrix controller to reconcile and enforce containment policies
        on the cluster.

        Run the following command against the target cluster, replacing
        `<chart-version>` with the current chart version from the
        [Aviatrix k8s-firewall-charts repository](https://aviatrixsystems.github.io/k8s-firewall-charts):

        ```bash theme={null}
        helm install --repo https://aviatrixsystems.github.io/k8s-firewall-charts --version <chart-version> k8s-firewall k8s-firewall
        ```
      </Step>

      <Step title="Verify Aviatrix CRD installation">
        The Helm chart installs two Aviatrix CRDs on the cluster. Verify both are
        present before continuing:

        ```bash theme={null}
        kubectl get crds | grep aviatrix
        ```

        Expected output:

        ```
        firewallpolicies.networking.aviatrix.com        <timestamp>
        webgrouppolicies.networking.aviatrix.com        <timestamp>
        ```

        If either CRD is missing, re-run the Helm installation and check the output for
        errors.
      </Step>

      <Step title="Onboard the Kubernetes cluster to Aviatrix DCF">
        1. Navigate to **Cloud Resources > Cloud Assets > Kubernetes Clusters**.

        2. Click **Onboard** next to the target cluster.

        3. Choose the appropriate access method for your cloud provider:
           * **AWS (EKS):** Select **Terraform** or **Command Line**, apply the
             generated access entry and RBAC configuration, check the confirmation box,
             then click **Onboard**.
           * **Azure (AKS):** Select **Permissions on Cloud Account** if the Aviatrix
             service principal has the required permissions, or **Kubeconfig File** to
             upload a kubeconfig. Click **Onboard**.

        4. Wait for the cluster status to show **Yes** (green) on the Kubernetes
           Clusters tab.

        <Note>
          EKS clusters require a `view-nodes` ClusterRole so the Controller can discover
          node metadata. The CoPilot onboarding dialog generates the required YAML
          automatically — apply it before clicking **Onboard**.
        </Note>
      </Step>

      <Step title="Configure V1 DCF policies and default deny">
        Before enabling default deny, configure V1 infrastructure permit rules so the
        cluster continues to function. These rules allow node-level traffic (container
        registry pulls, managed identity, AKS platform services) that must be permitted
        independently of per-pod MCP containment policies.

        **Create SmartGroups**

        Navigate to **Security > Distributed Cloud Firewall > Groups > SmartGroups** and
        create:

        **`aks-subnet`**: source for infrastructure rules

        * Match type: **CIDR**
        * CIDR: the AKS node subnet address range (for example, `10.0.0.0/24`)

        **`k8s-api-server`**: Kubernetes API server public IP

        * Match type: **CIDR**
        * CIDR: the AKS API server public IP as a /32 (for example, `1.2.3.4/32`)

        To retrieve the IP:
        `az aks show -n <cluster> -g <rg> --query fqdn -o tsv | xargs host`

        <Warning>
          The AKS API server public IP can change during cluster upgrades, node pool
          scaling, and control plane maintenance. Monitor this IP and update the
          SmartGroup CIDR when it changes. A stale `/32` entry causes a silent
          enforcement gap: pods lose Kubernetes API access without any policy-level
          error.
        </Warning>

        **`obot-system`**: Obot orchestration pods

        * Match type: **Kubernetes**
        * Namespace: the Obot system namespace (for example, `obot-system`)

        **Create WebGroups**

        Navigate to **Security > Distributed Cloud Firewall > Groups > WebGroups** and
        create:

        **`aks-infra-egress`**: AKS infrastructure domains required by all nodes and
        pods

        | Category             | Domains                                                                                                                     |
        | -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
        | AKS API              | `*.hcp.<region>.azmk8s.io` (replace `<region>` with your Azure region slug, for example `uksouth`)                          |
        | Container registries | `*.azurecr.io`, `mcr.microsoft.com`, `*.data.mcr.microsoft.com`                                                             |
        | Azure storage        | `*.blob.core.windows.net`                                                                                                   |
        | Azure services       | `*.servicebus.windows.net`, `management.azure.com`, `packages.microsoft.com`, `acs-mirror.azureedge.net`, `*.aks.azure.com` |
        | Identity             | `login.microsoftonline.com`                                                                                                 |
        | OCI registries       | `ghcr.io`, `*.ghcr.io`, `pkg-containers.githubusercontent.com`                                                              |
        | Obot NPC chart       | `charts.obot.ai`                                                                                                            |

        **`obot-pod-egress`**: domains required by Obot orchestration pods

        * `charts.obot.ai`, `api.anthropic.com`, `github.com`, `*.github.com`,
          `raw.githubusercontent.com`, `*.githubusercontent.com`

        **Create V1 policy rules**

        Navigate to **Security > Distributed Cloud Firewall > Policy** and create the
        following rules in priority order:

        | Priority | Name                  | Source        | Destination                            | Protocol | Port | Action |
        | -------- | --------------------- | ------------- | -------------------------------------- | -------- | ---- | ------ |
        | 1        | `k8s-api-server-cidr` | `aks-subnet`  | `k8s-api-server`                       | TCP      | 443  | PERMIT |
        | 2        | `aks-infra-egress`    | `aks-subnet`  | Anywhere, WebGroup: `aks-infra-egress` | TCP      | 443  | PERMIT |
        | 3        | `obot-pod-egress`     | `obot-system` | Anywhere, WebGroup: `obot-pod-egress`  | TCP      | 443  | PERMIT |

        <Note>
          Priority 1 must be a plain CIDR permit, not a WebGroup rule. Pod traffic to
          `kubernetes.default.svc` is DNAT'd by kube-proxy to the AKS API server's
          public IP. TLS over an IP address has no SNI field in the ClientHello, so
          WebGroup domain matching skips it. Without this rule, all pods lose Kubernetes
          API access when default deny takes effect.
        </Note>

        **Enable default deny**

        Navigate to **Security > Distributed Cloud Firewall > Policy > Default Action**
        and set the default action to **Deny** with **Logging** enabled.

        <Note>
          Default deny must be set at POST\_RULES level, not as a V1 policy rule. A V1
          deny evaluates before the Kubernetes CRD enforcement block (K8S\_POLICY\_LIST).
          Setting deny in V1 would block all `FirewallPolicy` PERMIT rules generated by
          the network policy controller.
        </Note>
      </Step>

      <Step title="Configure Obot with the Aviatrix network policy provider">
        Add the Aviatrix network policy provider configuration to your Obot Helm
        release. Obot's PostStart hook reads these values and deploys the
        `aviatrix-network-policy-controller` chart into the cluster automatically.

        ```bash theme={null}
        helm upgrade obot obot \
          --repo https://charts.obot.ai \
          --namespace obot-system \
          --reuse-values \
          --set config.OBOT_SERVER_MCPNETWORK_POLICY_PROVIDER_CHART_REPO=https://charts.obot.ai \
          --set config.OBOT_SERVER_MCPNETWORK_POLICY_PROVIDER_CHART_NAME=aviatrix-network-policy-controller \
          --set config.OBOT_SERVER_MCPNETWORK_POLICY_PROVIDER_CHART_VERSION=<npc-version> \
          --set config.OBOT_SERVER_MCPDEFAULT_DENY_ALL_EGRESS=true
        ```

        Replace `<npc-version>` with the current chart version from
        [charts.obot.ai](https://charts.obot.ai).

        `OBOT_SERVER_MCPDEFAULT_DENY_ALL_EGRESS=true` ensures any MCP server deployed
        without declared egress domains starts with zero outbound access. Without this
        setting, servers with no egress declaration inherit the environment's default
        posture.

        After the upgrade, verify the network policy controller is running:

        ```bash theme={null}
        kubectl get pods -n obot-mcp
        ```

        <Note>
          The Aviatrix CRDs (`firewallpolicies.networking.aviatrix.com` and
          `webgrouppolicies.networking.aviatrix.com`) must be present before the network
          policy controller starts. If the controller pod shows a `CrashLoopBackOff`
          with a `no matches for kind FirewallPolicy` error, re-run the k8s-firewall
          Helm install from the earlier step.
        </Note>
      </Step>

      <Step title="Declare egress destinations per MCP server">
        Egress destinations are declared in the Obot server catalog, not in Kubernetes
        manifests. Each MCP server's allowed domains are part of its configuration in
        Obot.

        **Via the Obot admin UI:**

        When adding or editing an MCP server in the Obot admin catalog, set the **Egress
        Domains** field to a list of permitted destinations, for example
        `api.github.com` and `*.githubusercontent.com`.

        **Via the Obot API:**

        ```bash theme={null}
        curl -X POST http://<obot-host>/api/mcp-servers \
          -H "Content-Type: application/json" \
          -d '{
            "manifest": {
              "name": "github",
              "runtime": "npx",
              "npxConfig": {
                "package": "@modelcontextprotocol/server-github",
                "egressDomains": ["api.github.com", "*.githubusercontent.com"]
              }
            }
          }'
        ```

        Domain rules:

        * Bare hostname or leading wildcard only: `api.example.com` or `*.example.com`
        * No protocols, paths, ports, or IP addresses
        * `localhost`, `*.svc`, and bare `*` are rejected
        * Empty `egressDomains` with `OBOT_SERVER_MCPDEFAULT_DENY_ALL_EGRESS=true`
          results in zero outbound access for that server

        For the full egressDomains specification, see the
        [Obot MCP server egress control documentation](https://docs.obot.ai/configuration/mcp-server-egress-control/).
        Egress enforcement applies to HTTPS on TCP port 443 only; connections on other
        ports are blocked regardless of domain declarations.

        When Obot deploys the server, the network policy controller generates a
        `FirewallPolicy` CRD targeting that server's pods. Inspect generated policies:

        ```bash theme={null}
        kubectl get firewallpolicies -n obot-mcp
        ```

        Generated policy names follow the pattern `obot-<mnp-id>-fw`. Pod labels use an
        internal server ID (`app: <server-id>`, for example `app: ms16mfzb`), not the
        human-readable server name. This is expected.
      </Step>

      <Step title="Verify containment">
        1. Deploy an MCP server through Obot and confirm a `FirewallPolicy` was
           generated:

           ```bash theme={null}
           kubectl get firewallpolicies -n obot-mcp
           ```

        2. Check that the policy was applied successfully:

           ```bash theme={null}
           kubectl get events -n obot-mcp
           ```

           A successful apply produces an event with `Reason: UpdatePolicyListSuccess`.

        3. In CoPilot, navigate to **Security > Distributed Cloud Firewall** and select
           the cluster. Confirm the server's containment policy appears with the correct
           egress allow-list.

        4. To confirm egress blocking is active, attempt a connection from an MCP server
           pod to a destination not in its declared domains and verify it is denied. In
           CoPilot per-connection logs, a denied connection appears with **Action:
           Deny**. If no entry appears within a few seconds, verify that the spoke
           gateway is in the egress data path for the cluster's node subnet.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Terraform">
    <Steps>
      <Step title="Deploy an Aviatrix spoke gateway in the cluster VPC">
        An Aviatrix spoke gateway must be deployed in the same VPC as the target
        Kubernetes cluster. If a spoke gateway is already deployed in the cluster VPC,
        proceed to the next step.

        ```hcl theme={null}
        resource "aviatrix_spoke_gateway" "mcp_containment" {
          cloud_type   = var.cloud_type
          account_name = var.account_name
          gw_name      = "mcp-containment-spoke"
          vpc_id       = var.cluster_vpc_id
          vpc_reg      = var.region
          gw_size      = var.gateway_size
          subnet       = var.subnet_cidr
        }
        ```
      </Step>

      <Step title="Confirm network topology">
        The spoke gateway deploys into the gateway subnet; the AKS cluster sits in the
        node subnet of the same VNet. Use the diagram below to confirm subnet CIDRs and
        variable values before applying.

        <img src="https://mintcdn.com/aviatrix-14b37c43/SoyeMF4MpIwhhamV/images/guides/security/obot-mcp-egress-azure-topology.svg?fit=max&auto=format&n=SoyeMF4MpIwhhamV&q=85&s=5aed8f203ded46fadf5c05629ff04a18" alt="AKS VNet topology: node subnet with AKS cluster, gateway subnet with Aviatrix spoke gateway, egress routing path" width="1200" height="560" data-path="images/guides/security/obot-mcp-egress-azure-topology.svg" />
      </Step>

      <Step title="Disable Azure IP masquerade">
        Apply a ConfigMap that disables SNAT for all external destinations. Without
        this, the spoke gateway sees node IPs instead of pod IPs, and `FirewallPolicy`
        pod-selector rules have no effect.

        ```hcl theme={null}
        resource "kubernetes_config_map_v1" "ip_masq_config" {
          metadata {
            name      = "azure-ip-masq-agent-config"
            namespace = "kube-system"
            labels = {
              component                         = "ip-masq-agent"
              "kubernetes.io/cluster-service"   = "true"
              "addonmanager.kubernetes.io/mode" = "EnsureExists"
            }
          }
          data = {
            "ip-masq-agent" = <<-EOT
              nonMasqueradeCIDRs:
                - "0.0.0.0/0"
            EOT
          }
        }
        ```
      </Step>

      <Step title="Install the Aviatrix Kubernetes Firewall Helm chart">
        The Aviatrix Kubernetes Firewall Helm chart deploys the in-cluster components
        that allow the Aviatrix controller to reconcile and enforce containment policies
        on the cluster.

        Run the following command against the target cluster, replacing
        `<chart-version>` with the current chart version from the
        [Aviatrix k8s-firewall-charts repository](https://aviatrixsystems.github.io/k8s-firewall-charts):

        ```bash theme={null}
        helm install --repo https://aviatrixsystems.github.io/k8s-firewall-charts --version <chart-version> k8s-firewall k8s-firewall
        ```
      </Step>

      <Step title="Verify Aviatrix CRD installation">
        The Helm chart installs two Aviatrix CRDs on the cluster. Verify both are
        present before continuing:

        ```bash theme={null}
        kubectl get crds | grep aviatrix
        ```

        Expected output:

        ```
        firewallpolicies.networking.aviatrix.com        <timestamp>
        webgrouppolicies.networking.aviatrix.com        <timestamp>
        ```

        If either CRD is missing, re-run the Helm installation and check the output for
        errors.
      </Step>

      <Step title="Onboard the Kubernetes cluster to Aviatrix DCF">
        ```hcl theme={null}
        data "azurerm_kubernetes_cluster" "this" {
          name                = var.cluster_name
          resource_group_name = var.resource_group
        }

        resource "aviatrix_kubernetes_cluster" "this" {
          cluster_id          = lower(data.azurerm_kubernetes_cluster.this.id)
          use_csp_credentials = true
        }
        ```
      </Step>

      <Step title="Enable Kubernetes enforcement feature flags">
        Five controller feature flags are required for Kubernetes CRD enforcement. The
        `dcf_multi_policies` flag activates the K8S\_POLICY\_LIST evaluation block;
        `k8s_discovery` and `log_enrichment` enable pod identity resolution and
        per-connection logging.

        ```hcl theme={null}
        resource "null_resource" "k8s_dcf_features" {
          triggers = { controller_ip = var.controller_ip }

          provisioner "local-exec" {
            command     = <<-EOT
              set -euo pipefail
              CID=$(curl -sk "https://$${CONTROLLER}/v2/api" \
                -d "action=login&username=$${USERNAME}&password=$${PASSWORD}" \
                | python3 -c "import sys,json; print(json.load(sys.stdin).get('CID',''))")
              if [ -z "$${CID}" ]; then echo "ERROR: controller login failed" >&2; exit 1; fi
              for feature in k8s k8s_dcf_policies dcf_multi_policies k8s_discovery log_enrichment; do
                curl -sk "https://$${CONTROLLER}/v2/api" \
                  --data-urlencode "action=enable_controller_feature" \
                  --data-urlencode "CID=$${CID}" \
                  --data-urlencode "feature=$${feature}" > /dev/null
              done
              echo "DCF feature flags enabled"
            EOT
            interpreter = ["/bin/bash", "-c"]
            environment = {
              CONTROLLER = var.controller_ip
              USERNAME   = var.controller_username
              PASSWORD   = var.controller_password
            }
          }

          depends_on = [aviatrix_distributed_firewalling_config.enabled]
        }
        ```
      </Step>

      <Step title="Configure V1 DCF policies and default deny">
        Create infrastructure SmartGroups, WebGroups, V1 permit rules, and the
        POST\_RULES default deny. Traffic evaluation order is: V1 policy list →
        K8S\_POLICY\_LIST (FirewallPolicy CRDs) → POST\_RULES (default deny). V1 rules
        permit cluster infrastructure traffic before the Kubernetes CRD block evaluates
        MCP server policies.

        ```hcl theme={null}
        # Allow the controller's Cloud Asset Inventory to sync Kubernetes workload metadata
        # before creating SmartGroups that use k8s selectors.
        resource "time_sleep" "cai_sync" {
          create_duration = "30s"
          depends_on = [
            aviatrix_spoke_gateway.mcp_containment,
            aviatrix_kubernetes_cluster.this,
          ]
        }

        # SmartGroup: AKS node subnet
        resource "aviatrix_smart_group" "aks_subnet" {
          name = "aks-subnet"
          selector {
            match_expressions { cidr = var.aks_node_subnet_cidr }
          }
          depends_on = [time_sleep.cai_sync]
        }

        # SmartGroup: Kubernetes API server public IP (/32)
        resource "aviatrix_smart_group" "k8s_api_server" {
          name = "k8s-api-server"
          selector {
            match_expressions { cidr = "${var.aks_api_server_ip}/32" }
          }
          depends_on = [time_sleep.cai_sync]
        }

        # SmartGroup: Obot orchestration pods (K8s namespace selector)
        resource "aviatrix_smart_group" "obot_system" {
          name = "obot-system"
          selector {
            match_expressions {
              type          = "k8s"
              k8s_namespace = var.obot_namespace
            }
          }
          depends_on = [time_sleep.cai_sync]
        }

        # WebGroup: AKS infrastructure domains
        resource "aviatrix_web_group" "aks_infra_egress" {
          name = "aks-infra-egress"
          selector {
            match_expressions { snifilter = "*.hcp.${var.azure_region}.azmk8s.io" }
            match_expressions { snifilter = "*.azurecr.io" }
            match_expressions { snifilter = "*.blob.core.windows.net" }
            match_expressions { snifilter = "*.servicebus.windows.net" }
            match_expressions { snifilter = "mcr.microsoft.com" }
            match_expressions { snifilter = "*.data.mcr.microsoft.com" }
            match_expressions { snifilter = "management.azure.com" }
            match_expressions { snifilter = "login.microsoftonline.com" }
            match_expressions { snifilter = "packages.microsoft.com" }
            match_expressions { snifilter = "acs-mirror.azureedge.net" }
            match_expressions { snifilter = "*.aks.azure.com" }
            match_expressions { snifilter = "ghcr.io" }
            match_expressions { snifilter = "*.ghcr.io" }
            match_expressions { snifilter = "pkg-containers.githubusercontent.com" }
            match_expressions { snifilter = "charts.obot.ai" }
          }
        }

        # WebGroup: Obot orchestration pod domains
        resource "aviatrix_web_group" "obot_pod_egress" {
          name = "obot-pod-egress"
          selector {
            match_expressions { snifilter = "charts.obot.ai" }
            match_expressions { snifilter = "api.anthropic.com" }
            match_expressions { snifilter = "github.com" }
            match_expressions { snifilter = "*.github.com" }
            match_expressions { snifilter = "raw.githubusercontent.com" }
            match_expressions { snifilter = "*.githubusercontent.com" }
          }
        }

        # V1 policy list: infra permits evaluated before the K8S_POLICY_LIST block
        resource "aviatrix_distributed_firewalling_policy_list" "obot_mcp" {
          # P1: K8s API server CIDR permit
          # Pod→ClusterIP DNAT to API server IP has no SNI; WebGroup matching skips.
          # This CIDR rule must be priority 1 to preserve control-plane access.
          policies {
            name             = "k8s-api-server-cidr"
            action           = "PERMIT"
            priority         = 1
            protocol         = "TCP"
            logging          = true
            port_ranges { lo = 443 }
            src_smart_groups = [aviatrix_smart_group.aks_subnet.uuid]
            dst_smart_groups = [aviatrix_smart_group.k8s_api_server.uuid]
          }

          # P2: AKS infrastructure domain egress
          policies {
            name             = "aks-infra-egress"
            action           = "PERMIT"
            priority         = 2
            protocol         = "TCP"
            logging          = true
            port_ranges { lo = 443 }
            src_smart_groups = [aviatrix_smart_group.aks_subnet.uuid]
            dst_smart_groups = ["def000ad-0000-0000-0000-000000000001"] # Aviatrix anywhere group
            web_groups       = [aviatrix_web_group.aks_infra_egress.uuid]
          }

          # P3: Obot orchestration pod egress
          policies {
            name             = "obot-pod-egress"
            action           = "PERMIT"
            priority         = 3
            protocol         = "TCP"
            logging          = true
            port_ranges { lo = 443 }
            src_smart_groups = [aviatrix_smart_group.obot_system.uuid]
            dst_smart_groups = ["def000ad-0000-0000-0000-000000000001"] # Aviatrix anywhere group
            web_groups       = [aviatrix_web_group.obot_pod_egress.uuid]
          }

          depends_on = [aviatrix_distributed_firewalling_config.enabled]
        }
        ```

        <Note>
          Replace the placeholder UUID `def000ad-0000-0000-0000-000000000001` in the policy list above with the actual UUID for the **Aviatrix Anywhere** SmartGroup from your controller. Retrieve it from **Security > Distributed Cloud Firewall > Groups > SmartGroups** in CoPilot, or via the Aviatrix API.
        </Note>

        ```hcl theme={null}
        # Default deny at POST_RULES level.
        # POST_RULES evaluates after both V1 and K8S_POLICY_LIST. A V1 deny would
        # evaluate before K8S_POLICY_LIST and block all MCPNetworkPolicy PERMIT rules.
        resource "aviatrix_distributed_firewalling_default_action_rule" "deny_all" {
          action  = "DENY"
          logging = true

          depends_on = [aviatrix_distributed_firewalling_policy_list.obot_mcp]
        }
        ```
      </Step>

      <Step title="Configure Obot with the Aviatrix network policy provider">
        Add the Aviatrix network policy provider values to the Obot Helm release. Set
        `obot_version` to `0.21.0` or later. Replace `var.npc_chart_version` with the
        current chart version from [charts.obot.ai](https://charts.obot.ai).

        ```hcl theme={null}
        resource "helm_release" "obot" {
          name             = "obot"
          repository       = "https://charts.obot.ai"
          chart            = "obot"
          version          = var.obot_version  # minimum 0.21.0
          namespace        = var.obot_namespace
          reuse_values     = true

          values = [
            yamlencode({
              config = {
                OBOT_SERVER_MCPNETWORK_POLICY_PROVIDER_CHART_REPO    = "https://charts.obot.ai"
                OBOT_SERVER_MCPNETWORK_POLICY_PROVIDER_CHART_NAME    = "aviatrix-network-policy-controller"
                OBOT_SERVER_MCPNETWORK_POLICY_PROVIDER_CHART_VERSION = var.npc_chart_version
                OBOT_SERVER_MCPDEFAULT_DENY_ALL_EGRESS               = "true"
              }
            })
          ]

          depends_on = [
            helm_release.aviatrix_crds,  # CRDs must exist before NPC starts
            kubernetes_config_map_v1.ip_masq_config,
            aviatrix_distributed_firewalling_policy_list.obot_mcp,
            aviatrix_distributed_firewalling_default_action_rule.deny_all,
            null_resource.k8s_dcf_features,
          ]
        }
        ```
      </Step>

      <Step title="Declare egress destinations per MCP server">
        Egress destinations are declared in the Obot server catalog, not in Kubernetes
        manifests. Set them via the Obot API when creating or updating a server:

        ```bash theme={null}
        curl -X POST http://<obot-host>/api/mcp-servers \
          -H "Content-Type: application/json" \
          -d '{
            "manifest": {
              "name": "github",
              "runtime": "npx",
              "npxConfig": {
                "package": "@modelcontextprotocol/server-github",
                "egressDomains": ["api.github.com", "*.githubusercontent.com"]
              }
            }
          }'
        ```

        Domain rules:

        * Bare hostname or leading wildcard only: `api.example.com` or `*.example.com`
        * No protocols, paths, ports, or IP addresses
        * `localhost`, `*.svc`, and bare `*` are rejected
        * Empty `egressDomains` with `OBOT_SERVER_MCPDEFAULT_DENY_ALL_EGRESS=true`
          results in zero outbound access for that server

        For the full egressDomains specification, see the
        [Obot MCP server egress control documentation](https://docs.obot.ai/configuration/mcp-server-egress-control/).
        Egress enforcement applies to HTTPS on TCP port 443 only; connections on other
        ports are blocked regardless of domain declarations.
      </Step>

      <Step title="Verify containment">
        1. Apply the Terraform configuration and confirm no errors in the output.

        2. Deploy an MCP server through Obot and confirm a `FirewallPolicy` was
           generated:

           ```bash theme={null}
           kubectl get firewallpolicies -n obot-mcp
           ```

        3. Check that the policy was applied successfully:

           ```bash theme={null}
           kubectl get events -n obot-mcp
           ```

           A successful apply produces an event with `Reason: UpdatePolicyListSuccess`.

        4. In CoPilot, navigate to **Security > Distributed Cloud Firewall** and select
           the cluster. Confirm the server's containment policy appears with the correct
           egress allow-list.

        5. To confirm egress blocking is active, attempt a connection from an MCP server
           pod to a destination not in its declared domains and verify it is denied. In
           CoPilot per-connection logs, a denied connection appears with **Action:
           Deny**. If no entry appears within a few seconds, verify that the spoke
           gateway is in the egress data path for the cluster's node subnet.
      </Step>
    </Steps>
  </Tab>
</Tabs>
