Kubernetes deployment¶
Kysira ships in four integration models for Kubernetes and Envoy-based stacks. Choose the one that fits your infrastructure.
Prerequisites — image pull secret¶
The agent images are served from a private Google Artifact Registry. Create your pull credential in app.kysira.ai → Image Access → Add access → Kubernetes / Docker (see the Quickstart), save the JSON key, then create an imagePullSecret in the namespace you deploy into.
The username is the literal _json_key; the password is the key contents:
kubectl create secret docker-registry kysira-pull \
--docker-server=us-central1-docker.pkg.dev \
--docker-username=_json_key \
--docker-password="$(cat kysira-key.json)" \
-n kysira
Attach it to the service account your pods run as so every chart can pull without per-release configuration:
Alternatively, pass it per-release on each helm install below with --set imagePullSecrets[0].name=kysira-pull.
The three images live under us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/: kysira-ext-proc, kysira-inference, and kysira-nginx-auth-adapter.
Licensing (protected models)¶
If you're running a licensed model, kysira-inference needs a license key and an agent certificate — see Licensing for how to create both in app.kysira.ai.
- From the console, copy your license token and download your agent certificate bundle. The download is a single JSON file with
client_cert_pem/client_key_pem/chain_pemfields — see Licensing for thejqcommand that extracts each into its own.pemfile (extracting them any other way is a common source of malformed PEM files that get rejected downstream). - Combine your certificate with its CA chain into one file:
- Create a Kubernetes Secret with all three credentials:
kubectl create secret generic kysira-license \
--from-literal=token="<your license token>" \
--from-file=client-cert-chain.pem \
--from-file=client-key.pem \
-n kysira
- Point the chart at it, either with
--seton anyhelm install/upgradebelow:
--set "kysira-inference.licensing.enabled=true" \
--set "kysira-inference.licensing.secretName=kysira-license"
or in my-values.yaml under the umbrella chart:
kysira-inference:
licensing:
enabled: true
secretName: kysira-license
enforcement: permissive # switch to "enforce" once you're comfortable
Start with enforcement: permissive — the agent checks the license and logs the result, but boots either way. Once you've confirmed clean logs, switch to enforce so a denied license actually blocks the container from serving.
Model 1 — Standalone proxy¶
The Go proxy sits in front of your application and reverse-proxies all traffic. No changes to your app or infrastructure are required.
The proxy adds X-Kysira-Score, X-Kysira-Reason, and X-Kysira-Mode headers to every forwarded request so your app can log or react to threat scores if it wants. In shadow mode, X-Kysira-Would-Have-Killed: true is added when a request exceeds the score threshold.
Helm install¶
helm install kysira oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-platform \
--namespace kysira --create-namespace \
--set "kysira-proxy.config.targetURL=http://your-app:3000"
Or install the proxy chart alone (requires a separately running inference service):
helm install kysira-proxy oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-proxy \
--namespace kysira \
--set config.targetURL=http://your-app:3000 \
--set config.inferenceURL=http://kysira-inference:8081
kysira-proxy Helm values¶
| Value | Default | Description |
|---|---|---|
config.targetURL | http://localhost:3000 | Upstream application URL |
config.inferenceURL | (auto-derived) | Inference sidecar URL — defaults to http://<release>-kysira-inference:8081. Override only when inference is deployed separately. |
config.mode | shadow | shadow or active |
config.scoreThreshold | 0.95 | Kill threshold [0–1] |
config.sinks | file | Comma-separated sinks: file, stdout, http |
config.httpSinkURL | "" | Endpoint for the http sink (Datadog, Splunk, etc.) |
config.proxyPort | 8080 | Port to listen on |
persistence.enabled | true | Mount a PVC for events.jsonl |
persistence.size | 1Gi | PVC size |
metrics.enabled | true | Add Prometheus scrape annotations to the pod |
replicaCount | 1 | Number of proxy replicas |
kysira-inference Helm values¶
| Value | Default | Description |
|---|---|---|
env.KYSIRA_DEVICE | cpu | cpu, cuda, or mps — models run offline, baked into the image |
resources.requests.memory | 1Gi | Minimum memory (models are ~1.5 GB resident) |
persistence.enabled | false | Models are baked into the image; no PVC needed unless caching external models |
Model 2 — Istio ext_proc¶
kysira-ext-proc implements the Envoy External Processing gRPC protocol. The Istio sidecar calls it per request over gRPC — no additional proxy hop, lower latency.
Client → Envoy sidecar ──ext_proc gRPC──▶ kysira-ext-proc :50051
↕
kysira-inference :8081
↓
Your App
Helm install¶
helm install kysira-ext-proc oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-ext-proc \
--namespace kysira \
--set "envoyFilter.workloadSelector.app=your-app" \
--set config.inferenceURL=http://kysira-inference:8081
The chart creates an EnvoyFilter resource in the istio-system namespace (configurable via envoyFilter.namespace). The filter inserts the ext_proc HTTP filter into every sidecar matching workloadSelector, just before the router filter.
kysira-ext-proc Helm values¶
| Value | Default | Description |
|---|---|---|
config.inferenceURL | http://kysira-inference:8081 | Inference sidecar URL |
config.mode | shadow | shadow or active |
config.scoreThreshold | 0.95 | Kill threshold [0–1] |
config.redactKeys | "" | Extra comma-separated field-name substrings whose query/body values are replaced with {redacted} in decision logs, on top of the built-in defaults (token, secret, key, email, …). See observability.md. |
config.inferenceTimeoutMs | 300 | Milliseconds ext-proc waits for a score before failing open. Must stay strictly below envoyFilter.messageTimeout — an inner timeout larger than the one wrapping it can never fire, so every slow request waits out the outer timeout instead. |
config.shadowAsync | true | In shadow mode, answer Envoy immediately and score in the background so scoring adds no request latency. Set "false" to score inline as a latency rehearsal before switching config.mode to active. Ignored when mode is active. |
config.shadowWorkers | 32 | Concurrent background scorers when shadowAsync is on. Also bounds ext-proc's connection pool to inference, so raise it in step with inference's own KYSIRA_MAX_CONCURRENCY rather than alone. |
config.shadowQueue | 2048 | Background scoring queue depth. When full, jobs are dropped rather than delaying traffic; watch kysira_extproc_shadow_dropped_total. Sustained drops mean inference is undersized, not that this is too small. |
config.maxBodyBytes | 1048576 | Cap on the request body accumulated for scoring. Larger bodies are truncated for scoring only and pass through untouched. |
config.breakerThreshold | 5 | Consecutive inference failures before ext-proc stops calling inference and fails open immediately, rather than each request waiting out inferenceTimeoutMs. "0" disables. |
config.breakerCooldownMs | 10000 | How long the circuit stays open before admitting one probe. |
service.grpcPort | 50051 | gRPC listen port |
service.metricsPort | 9090 | HTTP port for health + metrics + mode API |
envoyFilter.enabled | true | Create the EnvoyFilter resource |
envoyFilter.context | SIDECAR_INBOUND | Envoy listener context |
envoyFilter.workloadSelector | {} | Label selector for target workloads |
envoyFilter.namespace | istio-system | Namespace where the EnvoyFilter is created |
envoyFilter.messageTimeout | 500ms | Per-message gRPC timeout |
envoyFilter.failureModeAllow | true | Pass requests through if ext_proc is unreachable |
Fail-open behaviour¶
Both deployment models fail open: if the inference service is unreachable, the request is passed through unmodified (proxy logs the error; ext_proc behaviour is controlled by failureModeAllow). This prevents Kysira from becoming a single point of failure.
Failing open is about the verdict, not the wait. Envoy still holds the request while it waits for ext-proc to answer, so an inference service that is slow rather than down shows up as latency on every request even though nothing is being blocked. Two settings keep that off the request path:
config.shadowAsync(defaulttrue) — in shadow mode ext-proc answers Envoy before it scores, so inference latency never reaches the caller.config.breakerThreshold(default5) — once inference is failing, requests skip it entirely instead of each one waiting outinferenceTimeoutMs.
In active mode the wait is unavoidable — the verdict may be a 403 — so keep config.inferenceTimeoutMs tight and well under envoyFilter.messageTimeout.
Port exclusion for ext_proc¶
The ext_proc deployment has traffic.sidecar.istio.io/excludeInboundPorts: "50051" on its pod so Istio does not intercept the gRPC connections from Envoy sidecars — otherwise Envoy would route the ext_proc call through itself, creating a loop.
Model 3 — Standalone Envoy (without Istio)¶
kysira-ext-proc works with any Envoy deployment, not just Istio. If you run Envoy directly (as an API gateway, via Contour, or as a standalone edge proxy), configure the ext_proc filter in your envoy.yaml instead of using an EnvoyFilter CRD.
Deploy only the ext-proc service and inference — skip the EnvoyFilter resource by setting envoyFilter.enabled: false:
helm install kysira-inference oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-inference \
--namespace kysira \
--set config.device=cpu
helm install kysira-ext-proc oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-ext-proc \
--namespace kysira \
--set config.inferenceURL=http://kysira-inference:8081 \
--set envoyFilter.enabled=false
Then add the ext_proc filter to your Envoy config. The filter goes in the HTTP filter chain, before the router:
static_resources:
clusters:
- name: kysira_ext_proc
type: STRICT_DNS
http2_protocol_options: {} # gRPC requires HTTP/2
load_assignment:
cluster_name: kysira_ext_proc
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: kysira-ext-proc.kysira.svc.cluster.local
port_value: 50051
listeners:
- name: ingress
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http_filters:
- name: envoy.filters.http.ext_proc
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
grpc_service:
envoy_grpc:
cluster_name: kysira_ext_proc
timeout: 0.5s
failure_mode_allow: true # fail open if ext_proc is unreachable
processing_mode:
request_header_mode: SEND
request_body_mode: BUFFERED
response_header_mode: SKIP
response_body_mode: SKIP
message_timeout: 0.5s
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
# ... rest of HCM config (route_config, stat_prefix, etc.)
The gRPC cluster must have http2_protocol_options: {} — ext_proc uses gRPC which requires HTTP/2. All other behaviour (shadow/active mode, scoring, fail-open) is identical to the Istio integration.
Contour / Envoy Gateway¶
Contour and Envoy Gateway both support custom Envoy configuration via ExtensionService or EnvoyPatchPolicy resources. The kysira-ext-proc gRPC endpoint is the same — only the mechanism for injecting the filter differs per project. Refer to the Contour ExtensionService docs or Envoy Gateway EnvoyPatchPolicy docs for the specific API.
Model 4 — nginx via auth_request¶
Standard nginx cannot make inline HTTP calls, but it can delegate auth decisions to an external service using the ngx_http_auth_request_module (included in all nginx builds). Kysira ships a thin adapter — kysira-nginx-auth-adapter — that acts as the auth_request target.
nginx ──auth_request──▶ /_kysira/check (internal location)
│ proxy_pass
kysira-nginx-auth-adapter:8090/_kysira/check
│ POST /score/all
kysira-inference:8081
│
200 (pass) or 403 (block)
│
nginx ──proxy_pass──▶ your-app (if 200)
nginx ──403──▶ client (if 403, via error_page)
Deploy¶
helm install kysira-inference oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-inference \
--namespace kysira \
--set config.device=cpu
helm install kysira-nginx-auth-adapter oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-nginx-auth-adapter \
--namespace kysira \
--set config.inferenceURL=http://kysira-inference:8081 \
--set config.mode=shadow
nginx configuration¶
http {
# Buffer request bodies so auth_request can forward them to the adapter.
# Without buffering, POST body scoring is skipped (the adapter still scores
# URI + method + headers, which catches most GET-based attacks).
client_body_buffer_size 64k;
client_max_body_size 10m;
server {
listen 80;
location / {
# Delegate every request to the Kysira adapter before proxying.
auth_request /_kysira/check;
# Capture scoring headers from the adapter response so they can be
# forwarded to the upstream app as request headers.
auth_request_set $kysira_score $upstream_http_x_kysira_score;
auth_request_set $kysira_reason $upstream_http_x_kysira_reason;
auth_request_set $kysira_mode $upstream_http_x_kysira_mode;
proxy_set_header X-Kysira-Score $kysira_score;
proxy_set_header X-Kysira-Reason $kysira_reason;
proxy_set_header X-Kysira-Mode $kysira_mode;
proxy_pass http://your-app;
# Custom error page when the adapter returns 403 (active mode).
error_page 403 = @kysira_blocked;
}
# Internal-only location — nginx does not expose this externally.
location = /_kysira/check {
internal;
proxy_pass http://kysira-nginx-auth-adapter:8090/_kysira/check;
proxy_pass_request_body on;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header Content-Type $content_type;
# Never cache auth decisions.
proxy_no_cache 1;
proxy_cache_bypass 1;
}
location @kysira_blocked {
default_type application/json;
return 403 '{"error":"forbidden","detail":"request blocked by Kysira WAF"}';
}
}
}
kysira-nginx-auth-adapter Helm values¶
| Value | Default | Description |
|---|---|---|
config.inferenceURL | http://kysira-inference:8081 | Inference service URL |
config.mode | shadow | shadow (score + log, never block) or active (block above threshold) |
config.scoreThreshold | 0.95 | Kill threshold [0–1] |
service.port | 8090 | Port the adapter listens on |
service.metricsPort | 9090 | Prometheus metrics port |
metrics.enabled | true | Expose /metrics endpoint |
metrics.serviceMonitor.enabled | false | Create a Prometheus Operator ServiceMonitor |
replicaCount | 2 | Replicas — keep ≥ 2 so rolling updates don't cause auth failures |
Behaviour notes¶
Fail-open: If the adapter is unreachable, nginx's auth_request returns a 500 error. To prevent this from blocking traffic, add error_page 500 = @kysira_failopen; alongside the 403 handler and have it proxy through. Alternatively set proxy_next_upstream error on the check location.
Body scoring: proxy_pass_request_body on forwards the buffered body to the adapter. nginx buffers bodies up to client_body_buffer_size in memory; larger bodies spill to disk then forward. For APIs that receive large uploads, tune client_body_buffer_size or set the adapter to score URI-only for those paths.
Shadow vs active: In shadow mode the adapter always returns 200 — flagged requests are logged but not blocked. Switch to active with --set config.mode=active or update the ConfigMap directly.
Kustomize overlays¶
The kustomize/ directory (dev/prod/demo overlays, including the Juice Shop demo stack) is maintained in the kysira-demo repo, not here. Clone that repo and apply overlays from it:
kysira-demo/kustomize/
├── base/ # shared Deployment + Service manifests
├── overlays/
│ ├── dev/ # single replica, debug logging, emptyDir storage
│ ├── prod/ # HPA, PDB, resource limits, PVC storage
│ └── demo/ # Juice Shop + NodePort services (:30080, :30090)
Apply an overlay¶
git clone https://github.com/kysira/kysira-demo
cd kysira-demo
# Development (uses emptyDir — no PVC needed)
kubectl apply -k kustomize/overlays/dev
# Production
kubectl apply -k kustomize/overlays/prod
# Full demo with Juice Shop (minikube)
kubectl apply -k kustomize/overlays/demo
Namespace¶
All overlays deploy into the kysira namespace. Create it first if it does not exist:
Umbrella chart — kysira-platform¶
kysira-platform is a Helm umbrella chart that installs all four components (inference, proxy, dashboard, ext-proc) with one command and exposes a unified values.yaml. The chart's source and example values now live in kysira-demo, which owns the proxy + dashboard stack; only the kysira-inference and kysira-ext-proc sub-charts are maintained in this repo.
helm install kysira oci://us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/charts/kysira-platform \
--namespace kysira --create-namespace \
-f my-values.yaml
Sub-chart values are namespaced by component:
# my-values.yaml
kysira-proxy:
config:
targetURL: http://my-app:3000
mode: active
kysira-inference:
resources:
requests:
memory: 2Gi
kysira-dashboard:
replicaCount: 2
Prometheus metrics¶
All Kysira services expose a /metrics endpoint in Prometheus text format. Scrape annotations are added to pods when metrics.enabled: true in the respective Helm chart.
Proxy metric names (:8080/metrics)¶
| Metric | Type | Labels |
|---|---|---|
kysira_requests_total | Counter | action (passed/active_kill/shadow_kill/error/skipped) |
kysira_request_duration_seconds | Histogram | action |
kysira_flagged_total | Counter | — |
kysira_killed_total | Counter | — |
kysira_inference_errors_total | Counter | — |
ext-proc metric names (:9090/metrics)¶
| Metric | Type | Labels |
|---|---|---|
kysira_extproc_requests_total | Counter | action |
kysira_extproc_request_duration_seconds | Histogram | — |
kysira_extproc_flagged_total | Counter | — |
kysira_extproc_killed_total | Counter | — |
kysira_extproc_inference_errors_total | Counter | — |
kysira_extproc_active_streams | Gauge | — |
nginx-auth-adapter metric names (:9090/metrics)¶
Same metric names as ext-proc (kysira_extproc_*) — both services perform the same role in different integration models.
| Metric | Type | Labels |
|---|---|---|
kysira_extproc_requests_total | Counter | action |
kysira_extproc_request_duration_seconds | Histogram | — |
kysira_extproc_flagged_total | Counter | — |
kysira_extproc_killed_total | Counter | — |
kysira_extproc_inference_errors_total | Counter | — |
inference metric names (:8081/metrics)¶
| Metric | Type | Labels |
|---|---|---|
kysira_inference_requests_total | Counter | endpoint |
kysira_inference_errors_total | Counter | error |
kysira_inference_duration_seconds | Histogram | endpoint |
kysira_model_loaded | Gauge | — |