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
metadata.agentis the only source of agent identity. It's a CEL expression evaluated by the gateway, usually the JWT subject. The value must matchspec.subjects[].agentin oneAgentPolicy. Without it every call is denied with “no authenticated agent identity”.backendTLSpresents the gateway's client certificate. Mount Secretdiffhold-gateway-client(keystls.crt,tls.key,ca.crt) into the gateway. By default the certificate name must beagentgateway(grpc.tls.clientNames).jwtAuth.mode: strictmakes the gateway reject requests without a valid JWT. In agentgateway's default mode,optional, they get through; Diffhold then denies every tool call for lack of an identity, but nothing else on the gateway is protected.failureMode: failClosedblocks calls when Diffhold can't be reached. The admission webhook also fails closed for agent ServiceAccounts.
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.
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.