DEV Community

Michael Levan
Michael Levan

Posted on • Originally published at solo.io

Security Holes in MCP Servers and How To Plug Them

Model Context Protocol (MCP), has officially hit one year old as of November 25th and although there have been some amazing innovations within MCP, one issue still persists - the gaping security hole. This is no secret as just about every organization is talking about it. The long-running joke so far has been “The S in MCP stands for security”.

Aside from prompt injections, MCP Server security is arguably the biggest issue in the AI security world right now.

In this blog post, you’ll learn how to fix the gaps.

Prerequisites

To follow along with this blog post, you should have:

  1. A Kubernetes cluster running (it can be local).
  2. Kubernetes Gateway API CRDs installed, which you can find here.

Why Security Matters For MCP

There are two forms of MCP servers:

  1. stdio
  2. StreamableHTTP

stdio (the communication method) stands for standard input/output. When using standard input/output, you’re typically targeting a pre-built MCP Server that’s written in, typically, Python or JS (Go is up and coming in this space) that’s called to locally via a command like uvx or npx (depending on how the MCP Server is built). It’s not a standard library download (like a pip install), but instead stored in cache (e.g - ~/.local/share/uv).

StreamableHTTP is an external (or even internal) server that you’re reaching out to that’s not cached locally. A good example of this is the GitHub Copilot MCP Server. It’s an MCP Server that you’re reaching out to over the HTTP protocol instead of via a local cache.

💡 There was another option called SSE which you may see, but it is now deprecated as of June 2025.

Regardless of which option you choose, there are many security holes.

One (of the many) problem with stdio MCP Servers is that you can't run them through a Gateway. That means no AuthN/Z, no rate limiting, and no tool control. The MCP Servers are effectively open and usable by anyone in an organization unless you’re manually locking each computer down with Claude Desktop configurations (which, spoiler alert: no ones doing).

With Streamable HTTP, you’re in the dark. You’re connecting Agents to some black box running in someones datacenter with who knows what (if any) security protocols, and even if there are security protocols, that doesn’t help from an overall AuthN/Z perspective for your organization. There’s also no way to even test the security without a proper pentest, which wouldn’t be legal without explicit permission from the organization hosting the MCP Server. The only way to do it would be to put an Agent in front of the MCP Server, but then you're not actually securing the MCP Server, you're securing the Agent.

As Model Context Protocol stands right now, there’s only one true way to secure it - with a proper AI Gateway.

Solo Enterprise For Agentgateway implements everything from locking down tools to proper user and system-level authentication to MCP Servers with or without Agents. In the following sections, you’ll see how to configure security for MCP.

Deploy An MCP Server and Agentgateway

The first step is to deploy an MCP Server and a Gateway via agentgateway enterprise so we not only have an MCP Server to test with, but a proper AI gateway to secure our MCP connectivity.

  1. Create a new Kubernetes Deployment pointing to the test MCP Server that is containerized. You’ll also see a Service that gets deployed so the Gateway can properly connect to it.
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-website-fetcher
  namespace: default
spec:
  selector:
    matchLabels:
      app: mcp-website-fetcher
  template:
    metadata:
      labels:
        app: mcp-website-fetcher
    spec:
      containers:
      - name: mcp-website-fetcher
        image: ghcr.io/peterj/mcp-website-fetcher:main
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-website-fetcher
  namespace: default
spec:
  selector:
    app: mcp-website-fetcher
  ports:
  - port: 80
    targetPort: 8000
    appProtocol: kgateway.dev/mcp
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Deploy the Backend so agentgateway knows what to route to. In this case, it’s routing to the MCP Server service that you deployed in step 1.
kubectl apply -f - <<EOF
apiVersion: gateway.kgateway.dev/v1alpha1
kind: Backend
metadata:
  name: mcp-backend
  namespace: gloo-system
spec:
  type: MCP
  mcp:
    targets:
    - name: mcp-target
      static:
        host: mcp-website-fetcher.default.svc.cluster.local
        port: 80
        protocol: StreamableHTTP
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Deploy the Gateway using the agentgateway enterprise class. This Gateway is what will be used for MCP Inspector to connect to (more on Inspector coming up).
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: agentgateway
  namespace: gloo-system
spec:
  gatewayClassName: agentgateway-enterprise
  listeners:
  - name: http
    port: 8080
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Same
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Create an HTTP route so you can route the traffic to the backend, which you created in step 3.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mcp-route
  namespace: gloo-system
spec:
  parentRefs:
  - name: agentgateway
  rules:
  - backendRefs:
    - name: mcp-backend
      group: gateway.kgateway.dev
      kind: Backend
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Capture the Gateway ALB IP in an environment variable to be used when connecting to the MCP Server. If you’re running this locally, you can do a port-forward on the Gateway and use localhost within the IP address section when connecting to it.
export GATEWAY_IP=$(kubectl get svc agentgateway -n gloo-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $GATEWAY_IP
Enter fullscreen mode Exit fullscreen mode
  1. Open MCP Inspector to connect to the MCP Server.
npx modelcontextprotocol/inspector#0.16.2
Enter fullscreen mode Exit fullscreen mode

The URL to put into MCP Inspector is: http://YOUR_ALB_LB_IP:8080/mcp or if you’re running locally, http://localhost:8080/mcp

You’re now connected to the MCP Server via Inspector (the MCP client), but as you can see, it’s fully open. There’s no security at all. In the next section, that’ll be fixed.

Secure MCP Server Auth

With a properly deployed MCP Server and agentgateway in front of it, let’s begin the journey of securing MCP Server connectivity. The first step is to enable token-based authentication. In this case, you’ll use a JWT token.

  1. Add in a traffic policy for auth based on a JWT token.
kubectl apply -f- <<EOF
apiVersion: gloo.solo.io/v1alpha1
kind: GlooTrafficPolicy
metadata:
  name: jwt
  namespace: gloo-system
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: agentgateway
  glooJWT:
    beforeExtAuth:
      providers:
        selfminted:
          issuer: solo.io
          jwks:
            local:
              key: '{"keys":[{"kty":"RSA","kid":"solo-public-key-001","use":"sig","alg":"RS256","n":"AOfIaJMUm7564sWWNHaXt_hS8H0O1Ew59-nRqruMQosfQqa7tWne5lL3m9sMAkfa3Twx0LMN_7QqRDoztvV3Wa_JwbMzb9afWE-IfKIuDqkvog6s-xGIFNhtDGBTuL8YAQYtwCF7l49SMv-GqyLe-nO9yJW-6wIGoOqImZrCxjxXFzF6mTMOBpIODFj0LUZ54QQuDcD1Nue2LMLsUvGa7V1ZHsYuGvUqzvXFBXMmMS2OzGir9ckpUhrUeHDCGFpEM4IQnu-9U8TbAJxKE5Zp8Nikefr2ISIG2Hk1K2rBAc_HwoPeWAcAWUAR5tWHAxx-UXClSZQ9TMFK850gQGenUp8","e":"AQAB"}]}'
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Save the token for auth via MCP Inspector.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNvbG8tcHVibGljLWtleS0wMDEifQ.eyJpc3MiOiJzb2xvLmlvIiwib3JnIjoic29sby5pbyIsInN1YiI6ImJvYiIsInRlYW0iOiJvcHMiLCJleHAiOjIwNzQyNzQ5NTQsImxsbXMiOnsibWlzdHJhbGFpIjpbIm1pc3RyYWwtbGFyZ2UtbGF0ZXN0Il19fQ.GF_uyLpZSTT1DIvJeO_eish1WDjMaS4BQSifGQhqPRLjzu3nXtPkaBRjceAmJi9gKZYAzkT25MIrT42ZIe3bHilrd1yqittTPWrrM4sWDDeldnGsfU07DWJHyboNapYR-KZGImSmOYshJlzm1tT_Bjt3-RK3OBzYi90_wl0dyAl9D7wwDCzOD4MRGFpoMrws_OgVrcZQKcadvIsH8figPwN4mK1U_1mxuL08RWTu92xBcezEO4CdBaFTUbkYN66Y2vKSTyPCxg3fLtg1mvlzU1-Wgm2xZIiPiarQHt6Uq7v9ftgzwdUBQM1AYLvUVhCN6XkkR9OU3p0OXiqEDjAxcg
Enter fullscreen mode Exit fullscreen mode
  1. Try to reconnect to the MCP Server and you’ll see an error similar to the below:
Connection Error - Check if your MCP server is running and proxy token is correct
Enter fullscreen mode Exit fullscreen mode
  1. Within MCP Inspector, click on Authentication and add in the following:
  2. Header Name: Authorization
  3. Bearer Token: Bobs Token from step 2

You should now be able to connect to the MCP Server successfully.

With proper auth set up, you now know that not just anyone can use your agentgateway to connect to an MCP Server. This allows you to ensure that the traffic you’re observing from an AuthN/Z perspective is valid in comparison to the “anyone can do whatever they want” nature of MCP Servers without agentgateway in place.

Locking Down MCP Tool Lists

The final step is to specify what MCP Tools are available. One of the main issues for organizations is they want to use Tools within an MCP Server, but not all Tools. For example, maybe a person or an AI Agent connecting to an MCP Server only needs the ability to view/list/get resources (like a readonly Agent), but with the current architecture out of the box available for MCP, that’s not doable.

However, with traffic policies via agentgateway, it is.

  1. Create a policy that specifies no tools available for use. This will help in testing the ability to lock down tools via the Gateway.
kubectl apply -f- <<EOF
apiVersion: gateway.kgateway.dev/v1alpha1
kind: TrafficPolicy
metadata:
  name: jwt-rbac
  namespace: gloo-system
spec:
  targetRefs:
    - group: gateway.kgateway.dev
      kind: Backend
      name: mcp-backend
  rbac:
    policy:
      matchExpressions:
        - 'mcp.tool.name == ""'
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Disconnect and reconnect via the MCP Inspector and you should see no tools available.

  1. Update the policy to include the fetch tool.
kubectl apply -f- <<EOF
apiVersion: gateway.kgateway.dev/v1alpha1
kind: TrafficPolicy
metadata:
  name: jwt-rbac
  namespace: gloo-system
spec:
  targetRefs:
    - group: gateway.kgateway.dev
      kind: Backend
      name: mcp-backend
  rbac:
    policy:
      matchExpressions:
        - 'mcp.tool.name == "fetch"'
EOF
Enter fullscreen mode Exit fullscreen mode
  1. Reconnect to the MCP Server via Inspector and you’ll now see the tool available.

Conclusion

With all of the security concerns around MCP Servers, it reminds us of a very important aspect of cyber security - it’s not about trying to block all bad actors, it’s about mitigating as much risk as possible. That should be the goal for every organization and with these implementations, your MCP security posture should be in a much better place.

Top comments (0)