Skip to content

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:

kubectl patch serviceaccount default -n kysira \
  -p '{"imagePullSecrets":[{"name":"kysira-pull"}]}'

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.

  1. 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_pem fields — see Licensing for the jq command that extracts each into its own .pem file (extracting them any other way is a common source of malformed PEM files that get rejected downstream).
  2. Combine your certificate with its CA chain into one file:
cat client-cert.pem chain.pem > client-cert-chain.pem
  1. 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
  1. Point the chart at it, either with --set on any helm install/upgrade below:
--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.

Client → kysira-proxy :8080 → Your App
                  ↕
          kysira-inference :8081

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 (default true) — in shadow mode ext-proc answers Envoy before it scores, so inference latency never reaches the caller.
  • config.breakerThreshold (default 5) — once inference is failing, requests skip it entirely instead of each one waiting out inferenceTimeoutMs.

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:

kubectl create namespace kysira

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 —