Topic: network segmentation security
MCP server network segmentation — egress filtering, allowlisted outbound calls, and network isolation
An MCP server that can make outbound HTTP requests to any URL the LLM constructs is one prompt-injection away from becoming an exfiltration proxy. The SSRF findings in the SkillAudit corpus — present in 36.7% of community MCP servers — represent exactly this: servers that pass LLM-supplied URLs directly to fetch() with no hostname validation or egress control. Five network segmentation patterns — applied at the code layer, the infrastructure layer, or both — close this attack class.
1. Application-layer egress allowlisting
The most reliable SSRF mitigation is to never call a URL that wasn't pre-approved. If your MCP server calls GitHub's API, there is no scenario where it needs to call 169.254.169.254, localhost, or a user-controlled hostname. Define the set of allowed targets at startup and validate every outbound call against it.
// Allowlist-based fetch wrapper
const ALLOWED_HOSTS = new Set([
'api.github.com',
'uploads.github.com',
'api.stripe.com',
])
// Private/reserved CIDR ranges to block
const PRIVATE_RANGES = [
/^127\./, // loopback
/^10\./, // RFC 1918
/^172\.(1[6-9]|2[0-9]|3[01])\./,// RFC 1918
/^192\.168\./, // RFC 1918
/^169\.254\./, // link-local / AWS metadata
/^::1$/, // IPv6 loopback
/^fc00:/, // IPv6 ULA
]
async function safeFetch(url: string, init?: RequestInit): Promise<Response> {
const parsed = new URL(url)
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
throw new Error(`SSRF protection: hostname ${parsed.hostname} not in allowlist`)
}
// Resolve and check IP (prevent DNS rebinding)
const { address } = await dns.promises.lookup(parsed.hostname)
if (PRIVATE_RANGES.some(r => r.test(address))) {
throw new Error(`SSRF protection: ${parsed.hostname} resolves to private IP ${address}`)
}
return fetch(url, init)
}
// NEVER pass LLM-supplied URLs directly to fetch:
// ❌ await fetch(toolArguments.url)
// ✅ await safeFetch(`https://api.github.com/repos/${owner}/${repo}`)
2. URL construction vs URL acceptance
The fundamental SSRF defense for MCP servers is the distinction between constructing a URL from trusted components and accepting a URL from an untrusted source (the LLM). An MCP server that takes a repoUrl tool argument and passes it to fetch() is in the second category.
The correct pattern is to accept structured parameters and construct the URL internally:
// VULNERABLE: accepts URL from LLM, passes to fetch
server.tool('fetch_url', { url: z.string().url() }, async ({ url }) => {
return fetch(url) // attacker can supply: http://169.254.169.254/latest/meta-data/
})
// SAFE: accepts structured params, constructs URL internally
server.tool('get_repo', {
owner: z.string().regex(/^[a-zA-Z0-9_.-]+$/).max(100),
repo: z.string().regex(/^[a-zA-Z0-9_.-]+$/).max(100),
}, async ({ owner, repo }) => {
// URL is constructed from allowlisted base + validated path components
return safeFetch(`https://api.github.com/repos/${owner}/${repo}`)
})
SkillAudit's SSRF scan specifically looks for tool handlers where a string parameter annotated with .url() or named url, endpoint, or target flows directly into a fetch call without hostname validation. This pattern accounts for over 60% of the SSRF findings in the corpus.
3. VPN-only deployment for internal services
For MCP servers that connect to internal corporate services (databases, internal APIs, intranet applications), the most effective network control is to deploy the server inside the corporate VPN with no direct internet egress. The server can reach internal services by design, but cannot exfiltrate data to an external attacker endpoint because outbound internet connections are blocked at the network level.
## docker-compose.yml for VPN-only deployment
services:
mcp-server:
image: myorg/internal-mcp-server:latest
networks:
- internal-only
environment:
- DATABASE_URL=postgresql://internal-db:5432/myapp
- INTERNAL_API_URL=http://internal-api:8080
# No external port exposure — accessed via Claude Code through internal network
# All dependencies are internal services
internal-db:
image: postgres:16
networks:
- internal-only
networks:
internal-only:
driver: bridge
internal: true # No external internet access from this network
The internal: true flag on a Docker network means containers on that network cannot initiate connections outside the network. This is a network-layer constraint that holds even if application-layer SSRF mitigations are misconfigured or bypassed.
4. Container network namespace isolation
When an MCP server runs in a container, Docker's default bridge network gives it access to the host network and all internet endpoints. Stricter isolation is possible by creating a dedicated bridge network with explicit service allowlisting:
## Create isolated network with specific DNS resolution
docker network create \
--driver bridge \
--opt com.docker.network.bridge.enable_ip_masquerade=false \
mcp-restricted
## Run server on restricted network — only explicit aliases resolve
docker run \
--network mcp-restricted \
--add-host github-api:140.82.112.5 \
--dns 8.8.8.8 \
myorg/mcp-server
## For Kubernetes: use NetworkPolicy to restrict egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mcp-server-egress
spec:
podSelector:
matchLabels:
app: mcp-server
policyTypes:
- Egress
egress:
- ports:
- port: 443
to:
- ipBlock:
cidr: 140.82.112.0/20 # GitHub API CIDR only
5. DNS filtering for residual gap coverage
DNS-layer filtering is not a substitute for application-layer validation, but it provides defense-in-depth coverage for cases where code-level controls are incomplete or bypassed. A DNS resolver that returns NXDOMAIN for known-malicious domains or known exfiltration endpoints stops the outbound connection before TCP is established.
## /etc/hosts blocking for development (minimal approach)
## Add to /etc/hosts on the development machine:
127.0.0.1 metadata.google.internal
127.0.0.1 169.254.169.254.xip.io
## Pi-hole or AdGuard Home with custom blocklist for team deployments:
## Add to your DNS blocklist:
/169.254.169.254/ # AWS/GCP/Azure metadata endpoints
/metadata.internal/
## Corporate: use split-horizon DNS to route *.internal to internal resolvers
## and route all other DNS through a filtering resolver (e.g., Cloudflare Gateway)
What SkillAudit checks for network security
SkillAudit's Security sub-score includes four network-level checks:
- SSRF via URL acceptance: Tool handlers that accept
url/endpoint/URL-shaped parameters and pass them tofetch()oraxioswithout hostname validation — HIGH finding - Private range blocking: Absence of a private-IP check in the fetch path — MEDIUM finding if the server makes any user-influenced outbound calls
- Redirect following without re-validation:
fetch(url, { redirect: 'follow' })can redirect a validated public URL to a private endpoint — MEDIUM finding - URL construction from tool arguments: Where string parameters are embedded directly in URLs without encoding/validation of path components — LOW finding (path traversal class)
For the Dockerfile and compose-file scanning that checks container-level network isolation, see MCP server container security. For the SSRF finding in SkillAudit's public scan of 100 community MCP servers, see the ecosystem report.
Check your server for SSRF
SkillAudit's Security scan includes automated SSRF detection — paste your GitHub URL to see the report in 60 seconds.
Run a free audit