Diffhold pre-alpha

Connect agentgateway

Diffhold runs as an ExtMcp remote guardrail processor. agentgateway sends every MCP request to it over mutual TLS, along with the authenticated agent identity.

Processor configuration

jwtAuth:
  mode: strict                      # the default, optional, admits requests without a JWT
  issuer: https://idp.example.com
  audiences: [agentgateway]
  jwks: {url: https://idp.example.com/.well-known/jwks.json}
mcpGuardrails:
- kind: remote
  host: diffhold.diffhold-system.svc:9000
  failureMode: failClosed
  metadata:
    agent: jwt.sub                # required: the authenticated agent identity
  policies:
    backendTLS:
      cert: /gw/tls.crt
      key: /gw/tls.key
      root: /gw/ca.crt
      hostname: diffhold.diffhold-system.svc

The MCP server

On an allowed call, Diffhold sets Authorization: Bearer <per-call token> on the request to the MCP server, replacing the agent's JWT. kubernetes-mcp-server uses that header as its Kubernetes credential. Give it no credentials of its own: no ServiceAccount token, and a kubeconfig with only the API server address and CA.

# kubernetes-mcp-server config.toml
port = "8080"
kubeconfig = "/etc/mcp/kubeconfig"     # server + CA only, no user

# Deployment
automountServiceAccountToken: false

Diffhold's dry-run applies under the field manager the MCP server uses, kubernetes-mcp-server by default, so an apply that removes fields is predicted exactly. For another MCP server, set mcpServer.tokenHeader and mcpServer.applyFieldManager in the chart.

Tested end to end with agentgateway 1.7.0 and kubernetes-mcp-server 0.1.1 on kind, in CI on every push: reads, scale, server-side apply including field removal, denials, a held delete approved and executed once, and held exec.

What gets checked

Diffhold handles tools/call requests and passes other MCP methods through. Tool names are matched without the gateway's target prefix, so k8s:resources_scale is treated as resources_scale. The kubernetes-mcp-server tool set is mapped: resources_* (get, list, create or update with single or multi-document YAML, scale, delete), pods_* (including exec, logs and run), events, namespaces and nodes_top. nodes_log and nodes_stats_summary are denied because they read through nodes/proxy, which the backstop refuses for agent tokens. helm_install, helm_uninstall and helm_list are denied in v0.1: one install can create dozens of objects, and Diffhold can't yet show and approve all of them, and helm_list reads Helm's release Secrets, and can do so across namespaces, which the adapter can't yet map safely. Chart support is planned for v0.2. Until then, let agents change charts through GitOps: a Flux HelmRelease or Argo CD Application is an ordinary object, so policy can limit its chart version and values like any other apply. Any tool Diffhold doesn't know is denied.

Other gateways

Gateways without ExtMcp can call the AuthZEN endpoint, POST /access/v1/evaluation on the admin port, with the admin token. It returns the decision and the diff but no token, so writes made after an AuthZEN allow are still rejected by the backstop. Use it for decisions and dry-runs, not as the enforcement path.

Testing without TLS

For local work only, diffhold serve accepts plaintext on a loopback address, or anywhere with --insecure-grpc, and --insecure-agent-header x-agent-id takes the identity from a header. Both let any client claim to be any agent. Never use them in a shared cluster.