Companion to
docs/architecture/a2a-gateway.md. Read that first.
The gateway is opt-in. To turn it on:
helm upgrade kars deploy/helm/kars \
--set a2aGateway.enabled=true \
--set inferenceRouter.a2aMtls.enabled=true # required pairBoth halves must agree:
a2aGateway.enabled=truerenders the gateway Deployment, Service, and ServiceAccount.- The router's mTLS listener is gated by the env var
A2A_MTLS_ENABLED=1(set today on the router Deployment when the pair is opted in).
When a2aGateway.enabled=false (default), the inference router is
reachable only on the cluster-internal mesh — current behaviour
byte-for-byte unchanged.
Two independent rotations:
- cert-manager / AGC controller writes the new leaf to the Secret
named in
a2aGateway.tls.secretName. - The Kubelet refreshes the Secret-projected volume (≤60s).
- The gateway's
notify::Watcher(a2a-gateway/src/tls.rs) sees the file-change event and atomically swapsArc<ServerConfig>. - Existing TLS sessions continue under the old key; new sessions pick up the new leaf. No pod restart required.
- Rotate the gateway client cert (
a2aGateway.mtls.secretName) and the corresponding CA bundle the router trusts (A2A_MTLS_CA_PATHon the router Deployment) in lockstep. - Use a transitional CA bundle (old + new) for at least one Kubelet
refresh window before retiring the old cert. The Cilium
ClusterwideNetworkPolicy
kars-a2a-gateway-to-routeralready restricts the path to the gateway ServiceAccount, so a stolen cert from outside that SA cannot reach the router even if chain-of-trust check passed.
The defaults (60 burst / 5 rps refill) are a starting point.
To tune:
a2aGateway:
rateLimits:
perSubjectBurst: 120
perSubjectRefillPerSec: 10
maxSubjects: 100000Indicators that you need to raise the limits:
- Prometheus:
a2a_gateway_rejections_total{reason="rate_limited"}rising faster thana2a_gateway_requests_total. - Reports of 429 responses from legitimate peers in their post-mortems.
Indicators you can lower them:
- Subject map nearing
maxSubjectsfrom a small set of repeat callers (i.e., the cap was sized for breadth that does not exist).
The Helm value sharedRedisUrl is reserved for cross-replica sync
but not yet implemented. Setting it today panics the
gateway with an explicit message.
| Endpoint | Port | Use |
|---|---|---|
/healthz |
9090 | Liveness — process up. |
/readyz |
9090 | Readiness — admin server bound. |
/metrics |
9090 | Prometheus exposition. |
- job_name: kars-a2a-gateway
kubernetes_sd_configs:
- role: pod
selectors:
- role: pod
label: "app.kubernetes.io/name=kars-a2a-gateway"
relabel_configs:
- source_labels: [__meta_kubernetes_pod_container_port_name]
regex: admin
action: keep| Alert | Expression | Severity |
|---|---|---|
| Gateway down | up{job="kars-a2a-gateway"} == 0 for 5m |
page |
| JWS rejection burst | rate(a2a_gateway_rejections_total{reason="jws_invalid"}[5m]) > 1 |
ticket |
| Replay attack signal | rate(a2a_gateway_rejections_total{reason="replay"}[1m]) > 0 |
page |
| Subject map saturating | a2a_gateway_subject_count / 50000 > 0.8 |
ticket |
| Symptom | Likely cause | Fix |
|---|---|---|
503 on /readyz |
Admin server not bound | Check pod kubectl logs for bind errors. |
All requests rejected as jws_invalid |
Trust store empty | Check A2aAgent CR projection / re-apply trust anchors. |
| Router refuses gateway connections | CA bundle skew | Verify A2A_MTLS_CA_PATH on the router contains the current gateway-CA. |
| Cert reload didn't fire | macOS dev cluster (kqueue quirks) | Force a pod restart; in-cluster Linux uses inotify and is reliable. |