From e9997c360e3c7cd5a13e488d0e7f658a182ac0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 18:09:16 +0200 Subject: [PATCH] feat(mcp): authorise tool-call parameters, not just the tool name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ACL answers 'may this identity call create-product'. Nothing answered 'may it call it with these arguments' — write authorises writing, not this particular record. And the read cap counts calls, so one call asking for a hundred thousand rows cost the same as one asking for ten. The OPA plugin sends the decoded JSON-RPC body, so the decision point sees the tool name and its arguments. The policy lives once in k8s-platform/opa and every domain asks the same server. Enforced at the gateway rather than in an agent, because an agent that has been talked into something is the case this exists for. Co-Authored-By: Claude Opus 5 --- kong/kustomization.yaml | 1 + kong/mcp/opa.yaml | 27 +++++++++++++++++++++++++++ kong/mcp/pluginbindings.yaml | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 kong/mcp/opa.yaml diff --git a/kong/kustomization.yaml b/kong/kustomization.yaml index 1d7e17a..c923648 100644 --- a/kong/kustomization.yaml +++ b/kong/kustomization.yaml @@ -18,6 +18,7 @@ resources: - mcp/ai-mcp-proxy.yaml - mcp/openid-connect.yaml - mcp/rate-limiting.yaml + - mcp/opa.yaml - mcp/audit-log.yaml - mcp/audit-log-body-capture.yaml - mcp/audit-log-body-capture-bindings.yaml diff --git a/kong/mcp/opa.yaml b/kong/mcp/opa.yaml new file mode 100644 index 0000000..0a103e4 --- /dev/null +++ b/kong/mcp/opa.yaml @@ -0,0 +1,27 @@ +# Authorisation on tool-call parameters, not just the tool name. +# +# The ACL answers "may this identity call create-product?". Nothing answered +# "may it call it with *these* arguments" — and `write` authorises writing, not +# this particular record. The read cap counts calls, so one call asking for a +# hundred thousand rows costs the same as one asking for ten. +# +# Enforced here rather than in an agent, because an agent that has been talked +# into something is exactly the case this exists for. The policy lives once, in +# k8s-platform/opa/policy.rego, and every domain asks the same server. +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-customers-mcp-opa + namespace: ${NAMESPACE} +plugin: opa +config: + opa_protocol: http + opa_host: opa.opa.svc.cluster.local + opa_port: 8181 + opa_path: /v1/data/kong + # The decision is about the arguments, so the arguments have to reach the + # decision point. Without this OPA sees a method and a path and nothing it + # can rule on. + include_parsed_json_body_in_opa_input: true + include_body_in_opa_input: false + include_route_in_opa_input: false diff --git a/kong/mcp/pluginbindings.yaml b/kong/mcp/pluginbindings.yaml index 22eca25..0ea608f 100644 --- a/kong/mcp/pluginbindings.yaml +++ b/kong/mcp/pluginbindings.yaml @@ -69,3 +69,21 @@ spec: group: configuration.konghq.com kind: KongRoute name: ${NAME}-customers-mcp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-customers-mcp-opa-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-customers-mcp-opa + targets: + routeRef: + group: configuration.konghq.com + kind: KongRoute + name: ${NAME}-customers-mcp