Skip to content

GCP Cloud Run deployment (Service Extensions)

This guide covers deploying Kysira when your stack is:

api.example.com → Google Cloud Application Load Balancer → Cloud Run

Kysira integrates as a Service Extensions traffic extension on the ALB. The LB forwards each request to kysira-ext-proc via the Envoy ext_proc gRPC protocol, which can inspect and optionally block the request before it reaches your Cloud Run service.


Architecture

                    ┌─────────────────────────────────────┐
                    │  Google Cloud Application LB         │
Internet ──────────▶│                                     │──────▶ Your Cloud Run service
                    │  Service Extension (ext_proc gRPC)  │
                    └──────────────┬──────────────────────┘
                                   │ gRPC (Envoy ext_proc protocol)
                          kysira-ext-proc  (Cloud Run)
                                   │ HTTP
                          kysira-inference  (Cloud Run)

The LB is the single integration point. One kysira-ext-proc instance covers all of your Cloud Run backends — you do not need a sidecar per service.


What you'll deploy

Two Cloud Run services, both stateless and CPU-only:

Service Image Role
kysira-inference Provided in the Kysira Admin Console Scores requests using ML models + regex detectors
kysira-ext-proc Provided in the Kysira Admin Console Receives gRPC callouts from the ALB, calls inference, returns verdict

Prerequisite — give Cloud Run access to the Kysira images

The Kysira release images live in Kysira's private Artifact Registry. Cloud Run pulls images as your project's Cloud Run service agent (a Google-managed identity), so the one-time setup is to grant that identity read access to our registry. You send Kysira your project number; we add the binding. No keys, no mirroring.

Which method do I use? (Cloud Run vs Kubernetes)

Your platform Use Why
Cloud Run Project number grant (this page) Cloud Run pulls as a Google service agent and can't present a key — so we grant that identity access instead.
Kubernetes Pull credential K8s presents an imagePullSecret at pull time, so a JSON key is exactly what it wants.
Cloud Run, restricted egress / want to pin Mirror (alternative below) Use a pull credential to copy images into your own registry first.

One-line rule: Kubernetes carries a key; Cloud Run can't — so Kubernetes gets a pull credential and Cloud Run gets a grant.

1. Find your project number

This is the numeric id of the project where Cloud Run runs (not the human-readable project id). It's not secret.

gcloud projects describe PROJECT_ID --format='value(projectNumber)'
# → e.g. 482910337544

2. Register it in the Admin Console

In the Kysira Admin Console open Image Access → Add access → Google Cloud Run, paste the project number, and submit. Kysira grants your Cloud Run service agent (service-PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com) roles/artifactregistry.reader on the releases repo.

If your project has never used Cloud Run

The service agent is created the first time you enable/use the Cloud Run API in the project. If the pull fails right after granting, deploy once (or enable the Cloud Run API), then retry — the grant activates as soon as the agent exists.

3. Use the Kysira image URLs directly

The Admin Console shows the full image paths. Use them as-is in the deploy commands below, in place of IMAGE_URL_FROM_KYSIRA_ADMIN_CONSOLE:

  • kysira-inferenceus-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/kysira-inference:TAG
  • kysira-ext-procus-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/kysira-ext-proc:TAG

No mirroring, no in-project repo — Cloud Run pulls straight from Kysira's registry once the grant is in place.


Alternative: mirror into your own registry

Prefer the project grant above for almost all cases. Mirror instead when you need to pin the exact image independently of your Kysira access, or when your environment has restricted egress that can't reach Kysira's registry at deploy time. Mirroring uses a pull credential (a JSON key) to copy the images into a repo in your own project, then Cloud Run pulls in-project.

Mirror steps

1. Get a pull credential. In the Admin Console open Image Access → Add access → Kubernetes / Docker, name it, and copy the JSON key — it is shown only once. Save it as kysira-key.json. The username is always _json_key; the JSON key is the password.

2. Authenticate Docker:

cat kysira-key.json | docker login -u _json_key --password-stdin https://us-central1-docker.pkg.dev

3. Create a repo in your project (skip if you already have one):

gcloud artifacts repositories create kysira \
  --repository-format=docker \
  --location=REGION \
  --description="Mirrored Kysira agent images"
gcloud auth configure-docker REGION-docker.pkg.dev --quiet

4. Mirror the images. Replace REGION, YOUR_PROJECT, and TAG:

SRC=us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases
DST=REGION-docker.pkg.dev/YOUR_PROJECT/kysira

for img in kysira-inference kysira-ext-proc; do
  docker pull  "$SRC/$img:TAG"
  docker tag   "$SRC/$img:TAG" "$DST/$img:TAG"
  docker push  "$DST/$img:TAG"
done

Then use REGION-docker.pkg.dev/YOUR_PROJECT/kysira/<image>:TAG in the deploy commands below. Same-project Cloud Run pulls from this repo with no extra IAM.


Deployment phases

Start in Phase 1 (observabilityMode) for every new environment. Promote to Phase 3 once you have signal and trust.

Phase LB behaviour Kysira mode Traffic risk
1 — Shadow (async) LB ignores the verdict; callout is fire-and-forget KYSIRA_MODE=shadow Zero — the LB never waits on Kysira
2 — Inline shadow LB waits on the verdict but Kysira always returns CONTINUE KYSIRA_MODE=shadow Low — fail-open protects traffic even if Kysira is unreachable
3 — Inline active LB acts on the verdict; Kysira blocks above threshold KYSIRA_MODE=active Managed — fail-open still active

Why Phase 1, not Phase 2?

Phase 1 (observabilityMode: true) is structurally incapable of affecting traffic — the LB never waits on Kysira and ignores its response. Phase 2 (inline + failOpen: true) protects against a Kysira outage but not against a miscalibrated model causing false positives. Phase 1 lets you validate scoring quality before any enforcement path exists.


Regional External Application LB required for Phase 1

observabilityMode: true is only supported by regional LbTrafficExtension resources. This guide provisions the callout backend service and traffic extension as regional, which requires a Regional External Application LB (a regional forwarding rule with load-balancing-scheme=EXTERNAL_MANAGED).

If you are running a Global External Application LB, skip Phase 1 and proceed directly to Phase 3. Global traffic extensions support inline enforcement (observabilityMode omitted) and work with all the same YAML fields — just substitute locations/global and global/forwardingRules / global/backendServices for the regional paths shown below.


Phase 1 — Shadow observation (observabilityMode)

1. Deploy kysira-inference

gcloud run deploy kysira-inference \
  --image IMAGE_URL_FROM_KYSIRA_ADMIN_CONSOLE \
  --region REGION \
  --port 8081 \
  --cpu 2 \
  --memory 2Gi \
  --min-instances 1 \
  --ingress internal \
  --allow-unauthenticated \
  --set-env-vars KYSIRA_DEVICE=cpu

Inference needs at least 2 GB RAM (models are ~1.5 GB resident). Set --min-instances 1 to avoid cold-start latency on the scoring path.

--ingress internal means Cloud Run rejects all requests from the public internet — only traffic that arrives through VPC is accepted. --allow-unauthenticated disables the IAM invoker check; the VPC ingress boundary is the security gate, so no OIDC token is required from ext-proc.

Licensing (protected models)

If you're running a licensed model, get a license key and an agent certificate from app.kysira.ai — see Licensing. When creating your license key, request an unbound license (Cloud Run has no cluster identity to bind to).

Store the credentials in Secret Manager:

cat client-cert.pem chain.pem > client-cert-chain.pem

echo -n "<your license token>" | gcloud secrets create kysira-license-token --data-file=-
gcloud secrets create kysira-license-cert --data-file=client-cert-chain.pem
gcloud secrets create kysira-license-key --data-file=client-key.pem

Then mount them and set the licensing env vars on the inference service:

gcloud run deploy kysira-inference \
  --image IMAGE_URL_FROM_KYSIRA_ADMIN_CONSOLE \
  --region REGION \
  --port 8081 \
  --cpu 2 \
  --memory 2Gi \
  --min-instances 1 \
  --ingress internal \
  --allow-unauthenticated \
  --set-env-vars "KYSIRA_DEVICE=cpu,LICENSE_ENFORCEMENT=permissive,AGENTS_BASE_URL=https://agents.kysira.ai,LICENSE_MODEL_ID=kysira/Argus-1,LICENSE_TOKEN_PATH=/etc/kysira/license/token,LICENSE_CLIENT_CERT_PATH=/etc/kysira/license/client-cert.pem,LICENSE_CLIENT_KEY_PATH=/etc/kysira/license/client-key.pem" \
  --set-secrets "/etc/kysira/license/token=kysira-license-token:latest,/etc/kysira/license/client-cert.pem=kysira-license-cert:latest,/etc/kysira/license/client-key.pem=kysira-license-key:latest"

Start with LICENSE_ENFORCEMENT=permissive — the agent checks the license and logs the result, but boots either way. Once you've confirmed clean logs, redeploy with LICENSE_ENFORCEMENT=enforce so a denied license actually blocks the container from serving.

2. Deploy kysira-ext-proc

# Get the URL of the inference service
INFERENCE_URL=$(gcloud run services describe kysira-inference \
  --region REGION --format 'value(status.url)')

gcloud run deploy kysira-ext-proc \
  --image IMAGE_URL_FROM_KYSIRA_ADMIN_CONSOLE \
  --region REGION \
  --port 50051 \
  --use-http2 \
  --cpu 1 \
  --memory 512Mi \
  --min-instances 1 \
  --no-allow-unauthenticated \
  --vpc-egress all-traffic \
  --network=YOUR_VPC_NETWORK \
  --subnet=YOUR_VPC_SUBNET \
  --set-env-vars "INFERENCE_URL=${INFERENCE_URL},KYSIRA_MODE=shadow,KYSIRA_SCORE_THRESHOLD=0.95"

--use-http2 is required — the ALB calls ext-proc over gRPC which requires HTTP/2.

--vpc-egress all-traffic routes all outbound traffic from ext-proc through your VPC. This is required for ext-proc to reach inference: *.run.app URLs resolve to public Google IPs, so with the default private-ranges-only egress those calls bypass the VPC and Cloud Run's ingress check on inference rejects them. With all-traffic, the calls go through VPC and are treated as internal.

Replace YOUR_VPC_NETWORK and YOUR_VPC_SUBNET with your VPC network name and subnet name (e.g. default and default). The subnet must be in the same region as the Cloud Run services.

3. Create the backend service for ext-proc

Service Extensions requires the callout target to be a backend service backed by a serverless NEG.

# Serverless NEG pointing to the ext-proc Cloud Run service
gcloud compute network-endpoint-groups create kysira-ext-proc-neg \
  --region=REGION \
  --network-endpoint-type=SERVERLESS \
  --cloud-run-service=kysira-ext-proc

# Regional backend service (HTTP/2 for gRPC)
# Must be regional — observabilityMode requires a regional traffic extension
gcloud compute backend-services create kysira-ext-proc-bs \
  --region=REGION \
  --protocol=HTTP2 \
  --load-balancing-scheme=EXTERNAL_MANAGED

gcloud compute backend-services add-backend kysira-ext-proc-bs \
  --region=REGION \
  --network-endpoint-group=kysira-ext-proc-neg \
  --network-endpoint-group-region=REGION

4. Configure the traffic extension

Save this as traffic-extension-phase1.yaml:

name: projects/PROJECT_ID/locations/REGION/trafficExtensions/kysira-waf
loadBalancingScheme: EXTERNAL_MANAGED
forwardingRules:
  - projects/PROJECT_ID/regions/REGION/forwardingRules/YOUR_FORWARDING_RULE
extensionChains:
  - name: kysira-chain
    matchCondition:
      celExpression: "true"   # inspect all requests
    extensions:
      - name: kysira-ext-proc
        authority: kysira-ext-proc
        service: projects/PROJECT_ID/regions/REGION/backendServices/kysira-ext-proc-bs
        timeout: 2s
        failOpen: true          # a Kysira outage never blocks your traffic
        observabilityMode: true # Phase 1: async — LB ignores the verdict
        supportedEvents:
          - REQUEST_HEADERS
          - REQUEST_BODY

Apply it:

gcloud network-services traffic-extensions import kysira-waf \
  --source=traffic-extension-phase1.yaml \
  --location=REGION

5. Verify

Check that Kysira is receiving traffic and scoring requests without affecting the LB:

# Stream ext-proc logs
gcloud run services logs read kysira-ext-proc --region REGION --tail 50

# Check inference health (no auth token needed — ingress is the boundary)
curl "${INFERENCE_URL}/health"

You should see score= log lines in ext-proc. No traffic impact to verify — in observabilityMode the LB is indifferent to Kysira's presence.

Inference is not reachable from your local machine

curl from your laptop will return HTTP 404 — Cloud Run's ingress is blocking the public request. This is correct behaviour. The health check above only succeeds from a machine on the same VPC (e.g. a Cloud Shell session or a VM in the same network).


Graduating to live enforcement (Phase 3)

Once you've validated scoring quality in Phase 1 (typically 1–2 weeks of traffic), move to inline enforcement.

1. Update the traffic extension to inline mode

Save as traffic-extension-phase3.yaml:

name: projects/PROJECT_ID/locations/REGION/trafficExtensions/kysira-waf
loadBalancingScheme: EXTERNAL_MANAGED
forwardingRules:
  - projects/PROJECT_ID/regions/REGION/forwardingRules/YOUR_FORWARDING_RULE
extensionChains:
  - name: kysira-chain
    matchCondition:
      celExpression: "true"
    extensions:
      - name: kysira-ext-proc
        authority: kysira-ext-proc
        service: projects/PROJECT_ID/regions/REGION/backendServices/kysira-ext-proc-bs
        timeout: 500ms          # tune based on your p99 inference latency
        failOpen: true          # a Kysira outage → pass through, not block
        # observabilityMode omitted (defaults false) — inline enforcement
        supportedEvents:
          - REQUEST_HEADERS
          - REQUEST_BODY
gcloud network-services traffic-extensions import kysira-waf \
  --source=traffic-extension-phase3.yaml \
  --location=REGION

The LB now waits on the Kysira verdict. failOpen: true means a Kysira outage or timeout causes the LB to pass the request through — not block it. This is the right default.

2. Kysira starts in shadow — no immediate enforcement

kysira-ext-proc defaults to KYSIRA_MODE=shadow. In inline mode with shadow, Kysira returns CONTINUE for every request while logging would-have-blocked decisions. This lets you verify inline latency and false positive rate before activating blocking.

# Get the URL of the ext-proc service
EXT_PROC_URL=$(gcloud run services describe kysira-ext-proc \
  --region REGION --format 'value(status.url)')

# Confirm shadow mode via the health endpoint
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  "${EXT_PROC_URL}/_kysira/health"
# → {"service":"kysira-ext-proc","status":"ok","mode":"shadow"}

3. Activate blocking

When you're ready to enforce, flip the mode. You can do this without a redeploy via the mode API:

curl -X POST \
  -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  -H "Content-Type: application/json" \
  -d '{"mode":"active"}' \
  "${EXT_PROC_URL}/api/mode"

Or set it permanently at deploy time:

gcloud run services update kysira-ext-proc \
  --region REGION \
  --update-env-vars KYSIRA_MODE=active

To revert to shadow instantly (no redeploy needed):

curl -X POST \
  -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  -H "Content-Type: application/json" \
  -d '{"mode":"shadow"}' \
  "${EXT_PROC_URL}/api/mode"

Environment variable reference

kysira-ext-proc

Variable Default Description
INFERENCE_URL http://localhost:8081 URL of the kysira-inference service
KYSIRA_MODE shadow shadow (log only) or active (block above threshold)
KYSIRA_SCORE_THRESHOLD 0.95 Kill threshold [0–1]
KYSIRA_LOG_BODY false Set to true to include body_sample in every decision log, not just flagged requests. Useful during initial rollout to validate what the model sees.
KYSIRA_REDACT_KEYS 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, …). Case-insensitive substring match. See observability.md.
INFERENCE_TIMEOUT_MS 2000 Milliseconds ext-proc will wait for an inference response before failing open. Increase if you see context deadline exceeded errors with large request bodies.
EXT_PROC_PORT 50051 gRPC listen port (set Cloud Run --port to match)
METRICS_PORT 9090 HTTP port for /_kysira/health and /metrics

kysira-inference

Variable Default Description
KYSIRA_DEVICE auto cpu, cuda, or mps — use cpu on Cloud Run
KYSIRA_SCORE_THRESHOLD (not used here) Threshold is enforced by ext-proc, not inference
KYSIRA_ARGUS_DISABLE "" Comma-separated Argus sub-models to suppress (e.g. generic_attack)
LICENSE_ENFORCEMENT off off, permissive (verify + log, still boot), or enforce (fail closed). See Licensing.
AGENTS_BASE_URL Kysira key-delivery endpoint. Set to https://agents.kysira.ai
LICENSE_MODEL_ID kysira/Argus-1 The model your license is entitled to
LICENSE_TOKEN_PATH /etc/kysira/license/token Path to the mounted license token
LICENSE_CLIENT_CERT_PATH /etc/kysira/license/client-cert.pem Path to the mounted certificate + CA chain
LICENSE_CLIENT_KEY_PATH /etc/kysira/license/client-key.pem Path to the mounted certificate private key

Regional co-location

Place kysira-ext-proc in the same region as your Cloud Run workloads. Since the traffic extension and backend service are regional, each region has its own independent stack. For multi-region deployments, repeat the Phase 1 and Phase 3 steps once per region — create a separate NEG, backend service, and traffic extension in each region, each pointing at that region's forwarding rule.


Fail-open security posture

failOpen: true means a Kysira outage, timeout, or error causes the ALB to pass the request through rather than block it. This is the correct default — it prevents Kysira from becoming a single point of failure for your production traffic.

The tradeoff: under a sustained Kysira outage, enforcement silently disappears. Mitigate with:

  • Cloud Run --min-instances 1 to eliminate cold starts on the scoring path
  • Alerting on kysira_extproc_inference_errors_total (exposed at METRICS_PORT/metrics)
  • timeout tuned conservatively relative to your observed inference p99

If your threat model requires fail-secure behaviour in active mode, set failOpen: false — but only after validating that Kysira's availability SLA meets your traffic SLA.


Common errors

gcloud run deploy fails with "invalid image" or "image not found"

The --image IMAGE_URL_FROM_KYSIRA_ADMIN_CONSOLE placeholder must be replaced with a real image URL — the Kysira releases path (us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/kysira-inference:TAG) if you used a project grant, or your mirrored URL (REGION-docker.pkg.dev/YOUR_PROJECT/kysira/kysira-inference:TAG) if you mirrored. See Prerequisite — give Cloud Run access to the Kysira images. The deploy fails immediately if the literal placeholder string is used.

gcloud run deploy fails with "permission denied" pulling the image

Cloud Run pulls as your project's service agent (service-PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com). If you used a project grant, confirm you sent the numeric project number of the project where Cloud Run runs, and that the project has used Cloud Run at least once (the service agent is created lazily). If you mirrored into a repo in a different project than the Cloud Run service, that project's service agent needs roles/artifactregistry.reader on the repo — mirroring into the same project avoids this.