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:
- HIGH: no NetworkPolicy in deployment manifests — server has unrestricted egress
- MEDIUM: no explicit block of cloud metadata service IPs
- LOW: no deny-all base policy — allow policies are additive rather than replacing a deny-all
For related patterns, see MCP server sandboxing and isolation and container escape prevention.