GCP Cloud Run deployment (Service Extensions)¶
This guide covers deploying Kysira when your stack is:
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.
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-inference→us-central1-docker.pkg.dev/cs-poc-uv5os9gxrjsncireus36uzd/kysira-agent-releases/kysira-inference:TAGkysira-ext-proc→us-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:
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:
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 1to eliminate cold starts on the scoring path - Alerting on
kysira_extproc_inference_errors_total(exposed atMETRICS_PORT/metrics) timeouttuned 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.