diff --git a/dev/flux-kustomization.yaml b/dev/flux-kustomization.yaml index 77f59ae..5818b55 100644 --- a/dev/flux-kustomization.yaml +++ b/dev/flux-kustomization.yaml @@ -24,6 +24,8 @@ spec: BACKEND_HOST: classic-models-api.classic-models.svc.cluster.local OIDC_ISSUER: https://keycloak.sttlab.local/realms/classic-models API_AUDIENCE: '["workforce-api"]' + # Read volume for an MCP caller, per identity per minute. + MCP_READS_PER_MINUTE: "60" BACKEND_AUDIENCE: '["classic-models-api"]' BACKEND_SCOPES: '[]' API_RATE_LIMIT_MINUTE: "60" diff --git a/kong/kustomization.yaml b/kong/kustomization.yaml index afc03db..1d7e17a 100644 --- a/kong/kustomization.yaml +++ b/kong/kustomization.yaml @@ -17,6 +17,7 @@ resources: - mcp/consumergroups.yaml - mcp/ai-mcp-proxy.yaml - mcp/openid-connect.yaml + - mcp/rate-limiting.yaml - mcp/audit-log.yaml - mcp/audit-log-body-capture.yaml - mcp/audit-log-body-capture-bindings.yaml diff --git a/kong/mcp/pluginbindings.yaml b/kong/mcp/pluginbindings.yaml index 14b493c..735b2b0 100644 --- a/kong/mcp/pluginbindings.yaml +++ b/kong/mcp/pluginbindings.yaml @@ -51,3 +51,21 @@ spec: group: configuration.konghq.com kind: KongService name: ${NAME}-workforce-mcp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-workforce-mcp-rate-limiting-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-workforce-mcp-rate-limiting + targets: + routeRef: + group: configuration.konghq.com + kind: KongRoute + name: ${NAME}-workforce-mcp diff --git a/kong/mcp/rate-limiting.yaml b/kong/mcp/rate-limiting.yaml new file mode 100644 index 0000000..e1989e5 --- /dev/null +++ b/kong/mcp/rate-limiting.yaml @@ -0,0 +1,37 @@ +# A read-volume cap per caller identity. +# +# A scope authorises a read and says nothing about volume. An agent reads a +# thousand records in seconds where a person clicks ten, and the difference is +# invisible to every control the estate had until now — the token is valid, the +# tool is permitted, the ACL passes. +# +# `identifier: credential` keys the counter on what the openid-connect plugin +# published as the caller's credential, which is the `sub` claim: the end user, +# whoever is asking on their behalf. So an agent looping under one person's +# identity spends that person's budget, and cannot spend everyone's. +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-workforce-mcp-rate-limiting + namespace: ${NAMESPACE} +plugin: rate-limiting-advanced +config: + identifier: credential + # Per-route counters. Sharing a namespace across domains would let a busy + # domain exhaust the budget for another. + namespace: ${NAME}-workforce-mcp + strategy: local + sync_rate: -1 + window_type: sliding + window_size: + - 60 + limit: + - ${MCP_READS_PER_MINUTE} + # Loud rather than silent: a caller that hits the cap is told, and the headers + # say when it resets. An agent that only sees a 429 with no Retry-After + # retries immediately and makes it worse. + hide_client_headers: false + error_code: 429 + error_message: >- + Read volume limit reached for this identity. This is a cap on how much can + be read per minute, not a permission problem.