Skip to content

gateway-guardrails

Helm chart for the public gateway of the Kubeflow docs-agent chatbot. This chart is the single source of truth for the Istio edge config — it replaces the raw kubectl_manifest heredocs that previously lived in terraform/istio_policies.tf and terraform/kagent_ingress.tf, and the standalone YAML under manifests/istio*.

What it manages

Layer Resources Toggle
TLS ClusterIssuer (Let's Encrypt) + Certificate gateway.tls.acme.enabled
Routing Gateway + VirtualService: only /api/session, the JWKS and the A2A stream are public (CORS lockdown, 300s stream timeout) gateway.enabled
Rate limit L1 a2a-global-ratelimit EnvoyFilter — 60 req/min token bucket on the :443 listener rateLimit.global.enabled
Rate limit L2 Envoy ratelimit service + Redis + EnvoyFilter — 10 req/min per client IP rateLimit.perIP.enabled (off: needs real client IPs, see below)
Mesh policies AuthorizationPolicies for the RAG stack (Milvus, MCP, embeddings, plus one per meshPolicies.llmInferenceServiceNames entry) meshPolicies.enabled

Why

The chatbot is backed by a single GPU-served LLM. Without limits, one script can exhaust GPU capacity (cost + DoS), and wide-open CORS let any origin embed it. These guardrails cap request volume and lock down who may call the endpoint — all at the Istio ingress, with no application changes.

Install / upgrade

The ACME HTTP-01 solver needs the istio IngressClass. Apply it once per cluster: kubectl apply -f ../../manifests/istio-tls/ingress-class.yaml. domain and gateway.tls.acme.email have no defaults. Terraform sets them from kagent_domain_name and kagent_acme_email; for a manual upgrade, pass them:

helm upgrade --install gateway-guardrails . -n docs-agent \
  --set domain=agent.your-domain.example \
  --set gateway.tls.acme.email=you@your-domain.example

To keep the contact email out of values (for example with Argo CD reading values from Git), create the ClusterIssuer once as a cluster admin and set gateway.tls.acme.createIssuer=false; the Certificate then references the existing clusterIssuerName. On a release that already created the issuer, first run kubectl annotate clusterissuer letsencrypt-prod helm.sh/resource-policy=keep so the upgrade does not delete it.

Session tokens are enforced (sessionAuth.enforce: true) on /a2a/* and /api/a2a/*: a request without a valid token from POST /api/session gets a 403.

Tuning examples:

--set rateLimit.global.requestsPerMinute=120
--set routing.cors.allowOrigins[0].exact=https://your-widget.example.app

Per-IP rate limit prerequisite

rateLimit.perIP ships disabled: the ingress Service runs externalTrafficPolicy: Cluster, so client IPs are SNAT'd to node IPs and a per-IP descriptor would act as one shared bucket — effectively a second, tighter global limit rather than per-client fairness. Before enabling, either set externalTrafficPolicy: Local on the ingress Service (and verify the LB health checks) or trust XFF via meshConfig.numTrustedProxies.

Verifying

# Exposure: kagent's admin API (no auth) must not be reachable; expect 404 for all
for p in / /api/agents /api/sessions /api/toolservers '/a2a/..%2fapi/sessions'; do
  curl -s -o /dev/null -w "%{http_code} $p\n" --path-as-is "https://$DOMAIN$p"; done

# Session auth: no token -> 403
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://$DOMAIN/a2a/docs-agent/kubeflow-docs-agent \
  -H 'content-type: application/json' -d '{}'

# CORS: a foreign origin must get no access-control-allow-origin header
TOKEN=$(curl -s -X POST https://$DOMAIN/api/session | jq -r .access_token)
curl -s -o /dev/null -D - https://$DOMAIN/api/a2a/docs-agent/kubeflow-docs-agent/.well-known/agent.json \
  -H "Authorization: Bearer $TOKEN" -H "Origin: https://evil.example" | grep -i access-control-allow-origin

# Rate limit: expect 429s past the per-minute cap (403 for the rest: no token)
seq 1 100 | xargs -P 25 -I{} curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST https://$DOMAIN/api/a2a/docs-agent/kubeflow-docs-agent \
  -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":"t","method":"x"}' | sort | uniq -c

# CORS: the allowed origin gets an allow-origin header; others do not
curl -s -o /dev/null -D - -X OPTIONS https://$DOMAIN/api/a2a/docs-agent/kubeflow-docs-agent \
  -H "Origin: https://your-widget.example.app" -H "Access-Control-Request-Method: POST" \
  | grep -i access-control-allow-origin

Note on the shared ingress gateway

knative-ingress-gateway and knative-local-gateway also select istio: ingressgateway (the agent → LLM path rides knative-local on the same pod). The rate-limit EnvoyFilter is therefore scoped to the :443 listener so it never applies to the knative / internal LLM listeners.


This page is docs-agent-mcp/charts/gateway-guardrails/README.md from the repository, included as is so there is one copy. Edit it there.