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. 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. Per-route namespace, so a busy catalog cannot exhaust another domain's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,11 @@ spec:
|
||||
BACKEND_AUDIENCE: '["classic-models-api"]'
|
||||
BACKEND_SCOPES: '[]'
|
||||
API_RATE_LIMIT_MINUTE: "60"
|
||||
# Read volume for an MCP caller, per identity per minute. An agent
|
||||
# reads a thousand records in seconds where a person clicks ten; this is
|
||||
# the only control that notices. Generous enough that a conversational
|
||||
# turn never meets it, tight enough that a loop does.
|
||||
MCP_READS_PER_MINUTE: "60"
|
||||
# The URI path is the collector's routing key: Fluent Bit tags the
|
||||
# record `catalog` and turns that into the Loki label domain=catalog.
|
||||
AUDIT_LOG_ENDPOINT: http://audit-log-collector.observability.svc.cluster.local/catalog
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,3 +51,21 @@ spec:
|
||||
group: configuration.konghq.com
|
||||
kind: KongService
|
||||
name: ${NAME}-catalog-mcp
|
||||
---
|
||||
apiVersion: configuration.konghq.com/v1alpha1
|
||||
kind: KongPluginBinding
|
||||
metadata:
|
||||
name: ${NAME}-catalog-mcp-rate-limiting-binding
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
controlPlaneRef:
|
||||
type: konnectNamespacedRef
|
||||
konnectNamespacedRef:
|
||||
name: ${CONTROL_PLANE_REF_NAME}
|
||||
pluginRef:
|
||||
name: ${NAME}-catalog-mcp-rate-limiting
|
||||
targets:
|
||||
routeRef:
|
||||
group: configuration.konghq.com
|
||||
kind: KongRoute
|
||||
name: ${NAME}-catalog-mcp
|
||||
|
||||
@@ -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}-catalog-mcp-rate-limiting
|
||||
namespace: ${NAMESPACE}
|
||||
plugin: rate-limiting-advanced
|
||||
config:
|
||||
identifier: credential
|
||||
# Per-route counters. Sharing a namespace across domains would let a busy
|
||||
# catalog exhaust the budget for payments.
|
||||
namespace: ${NAME}-catalog-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.
|
||||
Reference in New Issue
Block a user