Security·Kubernetes·Network Policy

Kubernetes NetworkPolicy for MCP servers: egress restrictions and lateral movement prevention

An MCP server with unrestricted network access is an SSRF amplifier: if the server can reach any IP on any port, a single prompt-injection vulnerability turns it into an internal network scanner. Kubernetes NetworkPolicy narrows the blast radius by specifying exactly which ingress and egress connections are allowed. With deny-by-default policies in place, a compromised MCP server can only reach the upstream APIs it was already authorized to call — not your database, not the metadata service, not other microservices.

Why network policy matters for MCP servers specifically

Standard microservices are called by human clients or other services via known paths. MCP servers are called by LLM agents whose inputs may contain adversarially-crafted content (prompt injection). An attacker who can inject into a prompt can potentially cause the MCP server to make arbitrary outbound connections — to AWS instance metadata (169.254.169.254), internal services, or attacker-controlled infrastructure.

NetworkPolicy is one of the three controls SkillAudit checks under the Security axis for server-side SSRF risk. The others are application-level URL validation and outbound proxy filtering. NetworkPolicy is the enforcement layer that makes application-level validation meaningful — even if URL validation has a bug, the network layer says no.

Pattern 1: deny-all default policy

The starting point for any MCP workload is a deny-all policy for both ingress and egress. Apply this to the namespace, then add specific allow policies for what actually needs access.

# Base deny-all policy — apply first in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: mcp-workloads
spec:
  podSelector: {}        # Applies to ALL pods in the namespace
  policyTypes:
    - Ingress
    - Egress
  # No ingress or egress rules = deny everything

After applying this policy, no pod in the namespace can accept or initiate any connection. You then add explicit allow rules for the specific connections that are required. This is an allowlist approach — connections are denied unless explicitly permitted.

Pattern 2: ingress from the MCP gateway only

MCP servers should accept connections only from the gateway or load balancer that fronts them. Denying direct-to-pod ingress from other services prevents an attacker who has compromised another pod in the cluster from calling your MCP server directly without going through authentication middleware.

# Allow ingress only from the mcp-gateway pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mcp-server-ingress
  namespace: mcp-workloads
spec:
  podSelector:
    matchLabels:
      app: mcp-server
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: mcp-gateway
      ports:
        - protocol: TCP
          port: 3000

Pattern 3: selective egress to allowed upstream APIs

This is the SSRF mitigation policy. The MCP server is allowed to call only specific external services — not arbitrary IPs. The policy lists the allowed destinations explicitly:

# Allow egress only to specific external APIs and internal services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mcp-server-egress
  namespace: mcp-workloads
spec:
  podSelector:
    matchLabels:
      app: mcp-server
  policyTypes:
    - Egress
  egress:
    # DNS resolution (required for any hostname-based egress)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

    # GitHub API — for MCP servers that access GitHub
    - to:
        - ipBlock:
            cidr: 140.82.112.0/20  # GitHub's IP range (update from their meta endpoint)
      ports:
        - protocol: TCP
          port: 443

    # Internal Secrets Manager sidecar (for credential retrieval)
    - to:
        - podSelector:
            matchLabels:
              app: secrets-agent
      ports:
        - protocol: TCP
          port: 8200

    # Internal structured logging sink
    - to:
        - podSelector:
            matchLabels:
              app: log-aggregator
      ports:
        - protocol: TCP
          port: 5044

This policy explicitly blocks access to the AWS instance metadata service (169.254.169.254), other pods in the cluster, and arbitrary internet IPs — even if the application code attempts those connections.

Pattern 4: block cloud metadata service explicitly

Even with a deny-by-default egress policy, add an explicit deny for the cloud metadata service as defense-in-depth. On AWS, GCP, and Azure, the metadata service at link-local IPs is the highest-value SSRF target because it can return IAM credentials.

# Explicit deny for cloud metadata IPs (defense-in-depth)
# Note: standard NetworkPolicy cannot "deny" specific IPs within an allow rule.
# Use a Cilium NetworkPolicy for fine-grained IP-level deny rules.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: block-cloud-metadata
  namespace: mcp-workloads
spec:
  endpointSelector:
    matchLabels:
      app: mcp-server
  egressDeny:
    - toCIDR:
        - 169.254.169.254/32   # AWS/GCP/Azure IMDS
        - 100.100.100.200/32    # Alibaba Cloud metadata
    - toCIDR:
        - 10.0.0.0/8           # Private RFC1918 — block all internal routing
        - 172.16.0.0/12
        - 192.168.0.0/16

Runtime visibility with Cilium

Standard Kubernetes NetworkPolicy is enforced but invisible — you can't see blocked connection attempts. Cilium's eBPF-based enforcement provides flow logs that show you which connections were attempted and denied:

# Enable Hubble flow logging in Cilium
# In cilium-config:
hubble-enabled: "true"
hubble-export-file-path: /var/log/cilium/flows.log

# Query flows for MCP server denied egress:
hubble observe --namespace mcp-workloads \
  --verdict DROPPED \
  --pod-label app=mcp-server \
  --output json | jq '.flow | {src:.source.pod_name, dst:.destination.ip, port:.l4.TCP.destination_port}'

Denied egress connections from your MCP server pods — especially to metadata IPs or unexpected external addresses — are high-confidence indicators of an active SSRF exploit attempt. Route Hubble flow logs to your SIEM for alerting.

NetworkPolicy and SkillAudit

SkillAudit evaluates network policy through documentation review and static analysis of Kubernetes manifests included in your repository. Findings in this area:

For related patterns, see MCP server sandboxing and isolation and container escape prevention.