feat(mcp): cap read volume 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 until now nothing
noticed: the token is valid, the tool is permitted, the ACL passes. That gap
stopped being theoretical the day an autonomous run started reading the whole
catalog on a schedule.

identifier: credential keys the counter on 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. Verified on
catalog: pm-user and ro-user hold separate counters.

Per-route namespace, so a busy domain cannot exhaust another's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-29 18:05:52 +02:00
parent ffa2b1a72d
commit c0799877ad
4 changed files with 58 additions and 0 deletions
+2
View File
@@ -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: '["sales-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"
+1
View File
@@ -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
+18
View File
@@ -51,3 +51,21 @@ spec:
group: configuration.konghq.com
kind: KongService
name: ${NAME}-sales-mcp
---
apiVersion: configuration.konghq.com/v1alpha1
kind: KongPluginBinding
metadata:
name: ${NAME}-sales-mcp-rate-limiting-binding
namespace: ${NAMESPACE}
spec:
controlPlaneRef:
type: konnectNamespacedRef
konnectNamespacedRef:
name: ${CONTROL_PLANE_REF_NAME}
pluginRef:
name: ${NAME}-sales-mcp-rate-limiting
targets:
routeRef:
group: configuration.konghq.com
kind: KongRoute
name: ${NAME}-sales-mcp
+37
View File
@@ -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}-sales-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}-sales-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.