MCP Server Security · Service Mesh · mTLS · Kubernetes
MCP server service mesh mTLS security — Istio PeerAuthentication strict mode, Linkerd mTLS, SPIFFE SVID, DestinationRule TLS settings, and sidecar-intercepted MCP traffic
Service mesh mutual TLS (mTLS) provides cryptographic identity verification for every inter-service connection in a Kubernetes cluster — ensuring that the MCP gateway's call to the tool executor actually reaches the tool executor, not an attacker who has compromised a pod in the same namespace. Without mTLS, service-to-service MCP traffic travels unencrypted and unauthenticated at the network layer. Application-layer JWT authentication is necessary but not sufficient: a compromised pod can eavesdrop on plaintext MCP traffic or impersonate another service without any application-layer credentials by exploiting network routing.
The gap between application auth and network auth
An MCP deployment typically authenticates at two layers: the user authenticates to the MCP gateway (via JWT/OAuth), and the gateway authenticates to downstream services (via internal service tokens or JWTs). But in a Kubernetes cluster without a service mesh, the network path between services is:
- Unencrypted — any pod in the same namespace can intercept TCP traffic via ARP spoofing or by running a network sniffer on the host network namespace
- Unauthenticated at the network level — a compromised pod can send requests that appear to come from a legitimate service by spoofing source IP or by routing requests through a malicious intermediary
- Trust-boundary-free — all pods in the same namespace have network reachability to all other pods, unless NetworkPolicy resources restrict it
Service mesh mTLS closes this gap by requiring each workload to present a cryptographic certificate (a SPIFFE SVID) on every connection. The receiving service verifies the certificate before accepting any traffic. Application-layer auth then runs on top of this authenticated channel.
Istio PeerAuthentication: enforcing mTLS STRICT mode
# Namespace-wide STRICT mTLS — rejects all plaintext traffic from any peer
# Apply this to every namespace that contains MCP components
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: mcp-mtls-strict
namespace: mcp-production
spec:
mtls:
mode: STRICT # PERMISSIVE allows plaintext (migration only); STRICT rejects it
---
# Per-workload override: allow plaintext from the health check probe
# (only if your load balancer health checks can't present a certificate)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: mcp-tool-executor-health-check
namespace: mcp-production
spec:
selector:
matchLabels:
app: mcp-tool-executor
mtls:
mode: STRICT
portLevelMtls:
8080: # health check port
mode: DISABLE # allow plaintext only on health check port
PERMISSIVE mode is a migration aid, not a long-term configuration. Istio's PERMISSIVE mTLS mode accepts both mTLS and plaintext connections — useful when incrementally rolling out sidecar injection to existing workloads. But leaving it in PERMISSIVE permanently means an attacker who compromises a pod without a sidecar (or a pod in a non-meshed namespace) can still reach your MCP services in plaintext. Set a hard deadline to move all MCP namespaces to STRICT, then remove the PERMISSIVE configuration.
DestinationRule: requiring mTLS on outbound connections
# DestinationRule: require ISTIO_MUTUAL TLS on all outbound connections
# from the MCP gateway to downstream services
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: mcp-gateway-mtls-outbound
namespace: mcp-production
spec:
host: "*.mcp-production.svc.cluster.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL # use Istio's own certificates (not application TLS)
---
# AuthorizationPolicy: only allow the MCP gateway service account
# to call the tool executor — layer on top of mTLS for workload-identity-based authz
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: tool-executor-authz
namespace: mcp-production
spec:
selector:
matchLabels:
app: mcp-tool-executor
rules:
- from:
- source:
principals:
# SPIFFE URI of the MCP gateway service account
- "cluster.local/ns/mcp-production/sa/mcp-gateway"
to:
- operation:
methods: ["POST"]
paths: ["/tools/*"]
SPIFFE SVIDs: workload identity bound to certificates
SPIFFE (Secure Production Identity Framework For Everyone) defines a standard for workload identity. Each workload (Kubernetes pod) gets a SPIFFE Verifiable Identity Document (SVID) — an X.509 certificate with a URI SAN of the form spiffe://trust-domain/ns/namespace/sa/service-account. Istio's SPIRE agent (or its built-in CA) issues SVIDs automatically to each sidecar proxy. The SVID is rotated on a short interval (default 24h in Istio, configurable to 1h or less).
# Verify the SPIFFE identity of an MCP peer from within the Istio sidecar
# The SVID for the MCP tool executor looks like:
# spiffe://cluster.local/ns/mcp-production/sa/mcp-tool-executor
# In an AuthorizationPolicy, you reference SVIDs directly:
spec:
rules:
- from:
- source:
# Only the MCP gateway's SVID is permitted to call this endpoint
principals: ["cluster.local/ns/mcp-production/sa/mcp-gateway"]
- source:
# Also allow the internal audit service
principals: ["cluster.local/ns/mcp-production/sa/mcp-audit"]
# Check the SVID of a running pod (from inside the sidecar):
# istioctl proxy-config secret <pod-name> -n mcp-production
Linkerd: automatic mTLS with zero configuration
Linkerd's mTLS approach differs from Istio's: mTLS is on by default for all meshed pods, with no PeerAuthentication resources to configure. The Linkerd control plane issues short-lived certificates (default validity 24h) to each sidecar proxy automatically. The tradeoffs:
| Feature | Istio | Linkerd |
|---|---|---|
| mTLS default | PERMISSIVE (must set STRICT) | Automatic for all meshed pods |
| Certificate issuance | Istio CA or external SPIRE | Linkerd control plane (Linkerd-managed CA) |
| Certificate lifetime | Configurable (default 24h) | 24h (non-configurable in stable) |
| AuthorizationPolicy | Full policy language with source principal, method, path | Server + ServerAuthorization resources (simpler but less expressive) |
| Operational complexity | High — many CRDs, control plane components | Low — simpler mental model, fewer failure modes |
# Linkerd: inject mesh into the MCP namespace
kubectl annotate namespace mcp-production linkerd.io/inject=enabled
# Linkerd ServerAuthorization: restrict mcp-tool-executor to only accept
# connections from the mcp-gateway service account
apiVersion: policy.linkerd.io/v1beta1
kind: ServerAuthorization
metadata:
name: tool-executor-authz
namespace: mcp-production
spec:
server:
selector:
matchLabels:
app: mcp-tool-executor
client:
meshTLS:
serviceAccounts:
- name: mcp-gateway
namespace: mcp-production
Service mesh mTLS is infrastructure auth — it does not replace application-layer token validation. mTLS verifies that the network connection came from the correct workload (service account). It does not verify that the request was triggered by an authorized user, carries a valid tenant token, or respects rate limits. The correct architecture layers both: service mesh mTLS for workload-to-workload authentication at the network layer, plus application JWT/OAuth for user-to-service authentication at the application layer.
SkillAudit findings for service mesh mTLS in MCP server deployments
See also: OAuth security · Session isolation · Zero-trust architecture