learntutorialcore6 minsince 1.6.0
Control MCP server egress on Kubernetes
Updated October 13, 2026
The problem
An MCP server is code you did not write, running with network access you did not scope. A NetworkPolicy can say which hosts its pod may reach, but not which tool calls may go there or what their arguments may carry. A gateway can judge the call, but a pod that can open its own sockets walks around it.
The deny
An MCPEgressPolicy does both. The operator compiles it into a NetworkPolicy
(or a CiliumNetworkPolicy, for hostnames), and pushes its tool and argument
rules to the gateway. Here the rules allow only echo, and refuse any call
whose arguments carry a GitHub token. Two calls through mcp-hangar 2.27.2:
# echo with a clean argument: allowed
"success": true "text": "hello"
# echo with a GitHub token in its argument: refused before the server is asked
"success": false
"error": "Tool call denied by egress policy"
"error_type": "EgressPolicyDeniedError"
The gateway’s log names the rule that decided it:
egress_policy_enforced action=deny rule_kind=arguments tool_name=echo
policy_id=sha256:89ab90c5df47266f
reasons=["tool 'echo' matched an allow rule",
"arguments contain a secret matching 'github-tokens'"]
The network half is checked on a real CNI on every operator change: on Calico,
TestCIDRRuleAllowsAndOtherDestinationsAreBlocked,
TestNamespaceDefaultDenyLimitsUnregisteredPods and
TestOperator_EgressPolicyBackstopIsEnforced connect from the server’s pod and
assert that what the policy does not name times out.
The config that does it
kubectl label namespace prod mcp-hangar.io/enforce-egress=true
apiVersion: mcp-hangar.io/v1alpha2
kind: MCPEgressPolicy
metadata:
name: gh-readonly
namespace: prod
spec:
mode: Enforce # Audit (the default) records; Enforce refuses
targetRef: {kind: MCPServer, name: github}
defaultAction: Deny
upstreams:
- name: github
match:
host: api.github.com # a hostname needs Cilium; a CIDR works on any CNI
tools:
allow: ["get_*", "list_*"]
arguments:
deny:
secretPatterns: [github-tokens, jwt]
maxPayloadBytes: 262144
The namespace label puts a default-deny under every MCP server there; the
policy opens exactly what it names. Precedence is deny, then requireApproval,
then allow, then defaultAction.
What it does not do
- It does not cut connections that are already open. A policy applied to a
running pod refuses new connections; established ones live until they close.
Restart the pod after switching to
Enforce. - A hostname needs Cilium. A vanilla NetworkPolicy cannot match a DNS name,
so under another CNI a hostname upstream is denied, not opened, and the policy
reports
Degraded/FQDNUpstreamsUnenforceable. - The gateway half sees only what passes through the gateway. Without the network half, a pod that skips Hangar reaches whatever its own NetworkPolicy allows. The namespace label is what makes the tool rules enforcement.
- The reasons are not on every record. They are on the event and the log line above; the audit record and the trace carry the verdict, the rule kind and the policy id.