Skip to main content

Extending the Istio WASM Plugin

Goal​

Understand the Keymate Istio WASM access plugin — its dual implementation model, configuration parameters, deployment via Kubernetes CRDs, request flow, and extension points — so you can integrate Keymate authorization into your Istio service mesh and customize the plugin for your workloads.

Audience​

Developers integrating Keymate authorization into Istio-managed services, or customizing the plugin behavior for specific deployment scenarios.

Prerequisites​

  • A Kubernetes cluster with Istio installed and sidecar injection enabled
  • Access to a running Access Gateway instance reachable from the mesh
  • Helm 3 for deploying the plugin chart
  • For WASM modifications: Go and TinyGo 0.33.0
  • For Lua modifications: basic Lua and Envoy filter knowledge

Before You Start​

The Keymate Istio plugin ships in two implementations:

ImplementationLanguageRuntimeBest For
WASMGo (compiled with TinyGo)Envoy WASM VMProduction deployments, isolation, async processing
LuaLua (Envoy HTTP Lua filter)Envoy Lua VMRapid prototyping, no compilation step

Both implementations intercept HTTP requests in the Envoy sidecar proxy, forward them to the Access Gateway for authorization, and allow or deny the request based on the response. The WASM plugin runs at the AUTHN phase (before authorization), making it the first filter in the request chain. The Lua plugin is inserted as INSERT_BEFORE on envoy.filters.http.router in the SIDECAR_INBOUND context.

Worked Example​

In this guide, you deploy the plugin to an Istio-enabled namespace, configure it to protect a service, understand the request flow, and learn where to extend the behavior.

Steps​

1. Understand the request flow​

When a request arrives at an Istio Envoy sidecar with the Keymate plugin enabled:

The plugin:

  1. Captures the original request headers, method, path, and body
  2. Checks the path against a configurable skip list (for health checks, metrics endpoints)
  3. Removes protected headers to prevent spoofing (Keops-Enforcer, Keops-Target-URI, Keops-Target-Method, and Keops-Client-Id, plus the older Keymate-* spelling of each)
  4. Adds its own Keops-* headers identifying the mesh enforcer, target URI, and HTTP method, plus the client ID when it can read one from the bearer token
  5. Dispatches an HTTP call to the Access Gateway via an Envoy cluster
  6. Interprets the response: the WASM variant treats any 2xx as allow, the Lua variant only an exact 200, and anything else means deny

2. Understand the configuration​

The plugin accepts the following configuration parameters:

ParameterTypeRequiredDefaultDescription
gateway_urlStringRecommended—Base URL of the Access Gateway instance. In the current version it sets the :authority (host) of the call to the gateway. The plugin starts without it, but set it so the host of that call is correct
endpointStringNo/authAuthorization endpoint path appended to gateway_url
timeoutIntegerNo5000Request timeout in milliseconds
cluster_nameStringNo—Currently ignored. The plugin always dispatches to the Envoy cluster named access_gateway_cluster, so the EnvoyFilter must define a cluster with exactly that name
skip_pathsString arrayNo—Request paths to skip authorization (for example, /health, /metrics)
note

The skip_paths parameter is unique to the Istio plugin. The APISIX plugin relies on APISIX route matching for path filtering. Use skip_paths to bypass authorization for health checks, readiness probes, and metrics endpoints. Matching uses prefix comparison — /health matches both /health and /healthz.

3. Deploy with the WASM implementation​

The WASM plugin is deployed via two Kubernetes resources: an Istio WasmPlugin CRD and an EnvoyFilter for cluster routing.

WasmPlugin defines the plugin binary and configuration:

WasmPlugin CRD
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: keops-access-gateway
spec:
selector:
matchLabels:
app: <YOUR_SERVICE>
url: oci://<REGISTRY>/keymate-istio-wasm:<VERSION>
phase: AUTHN
imagePullPolicy: Always
pluginConfig:
gateway_url: "http://access-gateway.example.svc.cluster.local:8080"
endpoint: "/gateway/api/v1/access/check-permission"
timeout: 5000
skip_paths:
- "/health"
- "/metrics"

EnvoyFilter defines the Envoy cluster that routes traffic to the Access Gateway:

EnvoyFilter for cluster routing
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: access-gateway-cluster
spec:
workloadSelector:
labels:
app: <YOUR_SERVICE>
configPatches:
- applyTo: CLUSTER
match:
context: SIDECAR_OUTBOUND
patch:
operation: ADD
value:
name: access_gateway_cluster
connect_timeout: 5s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: access_gateway_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: access-gateway.example.svc.cluster.local
port_value: 8080

Deploy both resources with Helm:

Deploy with Helm
helm upgrade --install keymate-plugin ./charts -n <NAMESPACE> \
--set wasm.enabled=true \
--set wasm.image=<REGISTRY>/keymate-istio-wasm:<VERSION>

4. Deploy with the Lua implementation​

The Lua plugin is deployed as an Envoy HTTP Lua filter via an Istio EnvoyFilter resource. No compilation or OCI image is needed — the Lua code is embedded inline in the EnvoyFilter.

Deploy Lua filter with Helm
helm upgrade --install keymate-plugin ./charts -n <NAMESPACE> \
--set lua.enabled=true

The Helm chart reads the Lua source from charts/lua/keops-access.lua and inserts it as an INSERT_BEFORE filter on envoy.filters.http.router, applying to SIDECAR_INBOUND context.

note

The Lua implementation has the Access Gateway cluster address and endpoint hardcoded in the source file. When customizing for your environment, update the config.auth_cluster, config.endpoint, and the :authority header in the dispatch_headers table.

5. Understand the headers​

Headers set by the plugin (sent to Access Gateway):

HeaderValueDescription
Keops-Enforcerlayer="mesh", tech="istio", version="<plugin-version>"Identifies the enforcer as an Istio mesh plugin
Keops-Target-URIOriginal request pathThe target URI for access rule matching
Keops-Target-MethodOriginal HTTP methodThe HTTP method for resource resolution
Keops-Client-IdClient ID from the bearer token (azp claim, then client_id, then clientId)Sent only when the plugin can read a client ID from the token. The plugin does not verify the token; the Access Gateway does
note

These headers were called Keymate-Enforcer, Keymate-Target-URI, Keymate-Target-Method, and Keymate-Client-Id in earlier versions. The plugin never sends the older names, and it removes a client-supplied copy of either spelling before it calls the Access Gateway.

Headers filtered (not forwarded to Access Gateway):

The plugin removes these headers from the forwarded request to prevent client-side spoofing:

  • Keops-Enforcer, Keops-Target-URI, Keops-Target-Method, Keops-Client-Id: set by the plugin itself
  • The older Keymate-Enforcer, Keymate-Target-URI, Keymate-Target-Method, and Keymate-Client-Id: dropped if a client sends them
  • HTTP/2 pseudo-headers (:method, :path, :authority, :scheme)
  • host, content-length, transfer-encoding, connection, keep-alive, expect

Headers added to the client response:

HeaderSourceDescription
Keops-DecisionAccess GatewayAuthorization decision result and authority source
Keops-Decision-LatencyAccess GatewayTotal decision processing time in milliseconds
Keops-Decision-Authority-LatencyAccess GatewayAuthority call latency in milliseconds
Keops-Decision-CacheAccess GatewayCache hit or miss indicator
Keops-GatewayAccess GatewayAccess Gateway version
Keops-Gateway-Auth-DurationPluginTotal plugin execution time in milliseconds
Keops-Gateway-Auth-StatusPluginPlugin outcome: GRANTED, DENIED, SKIPPED, or GATEWAY_FAILED

The plugin also accepts the five Access Gateway headers under their older Keymate-* names (for example Keymate-Decision) from an Access Gateway that predates the rename, and returns them to the client under the Keops-* names. When both spellings arrive, the Keops-* value wins.

note

Both the Istio and the APISIX plugin set the Keops-Gateway-Auth-Status header, so its presence alone does not tell you which plugin handled a request. It provides an explicit status label (GRANTED, DENIED, SKIPPED, GATEWAY_FAILED) that is useful for observability dashboards and log aggregation. Only the Istio plugin uses SKIPPED, set when the request path matches a skip_paths entry.

6. Understand error handling​

ConditionHTTP StatusResponse BodyKeops-Gateway-Auth-Status
Access Gateway unreachable (WASM)502{"error": "Authorization Service Unavailable (Dispatch Failed)"}GATEWAY_FAILED
Access Gateway timeout (WASM)502{"error": "Authorization Service Unavailable (No Headers)"}GATEWAY_FAILED
No response from the Access Gateway (Lua)502{"error": "Authorization Service Unavailable"}GATEWAY_FAILED
Access Gateway returns 2xx (WASM) or exactly 200 (Lua)—Request forwarded to serviceGRANTED
Access Gateway returns 500, 502, 503 or 504 (WASM)Same as gatewayGateway response bodyGATEWAY_FAILED
Access Gateway returns any other non-2xx status, such as 4xx (WASM)Same as gatewayGateway response bodyDENIED
Access Gateway returns any status other than 200, a 5xx included (Lua)Same as gateway{"error": "Access Denied", "details": "Authorization check failed"}DENIED
Path in skip list—Request forwarded without auth checkSKIPPED

The Access Gateway answers an allow with exactly 200 today, so the difference between the two variants has no visible effect yet.

For a path in the skip list, the WASM variant adds Keops-Gateway-Auth-Status: SKIPPED and Keops-Gateway-Auth-Duration to the client response. The Lua variant adds both headers to the request it forwards to the service instead, so the client does not receive them.

The Lua implementation returns a structured error body on deny: {"error": "Access Denied", "details": "Authorization check failed"}. The WASM implementation forwards the Access Gateway's response body as-is.

7. Extend the plugin​

To add custom behavior, modify the plugin source:

Adding skip path logic: The current implementation uses prefix matching (strings.HasPrefix in WASM, string.find with plain match in Lua). Expand the shouldSkip function to support wildcard patterns or regex matching.

Adding custom headers: Add new headers alongside the existing Keops-* headers in the dispatch section to pass additional context to the Access Gateway.

Custom error responses: Modify the sendDeny function (WASM) or the request_handle:respond call (Lua) to return custom error formats, additional headers, or different status codes.

Lua dynamic metadata: The Lua implementation uses Envoy dynamic metadata (envoy.filters.http.lua) to pass decision headers from the request phase (envoy_on_request) to the response phase (envoy_on_response). Add new metadata keys to expose additional data to your observability stack.

warning

After modifying the WASM plugin, recompile with TinyGo (make build), rebuild the OCI image (make image), and redeploy the WasmPlugin resource. The Lua plugin requires only an EnvoyFilter update — Helm will inline the new Lua source.

Validation Scenario​

Scenario​

You deploy the WASM plugin to a namespace with an echo server, send a request with a valid access token, and verify the authorization flow.

Expected Result​

  1. The echo server receives the request (HTTP 200)
  2. The client response includes Keops-Gateway-Auth-Status: GRANTED and Keops-Decision headers
  3. Requests to /health bypass authorization with Keops-Gateway-Auth-Status: SKIPPED

How to Verify​

  • API evidence: Send a request with a valid Bearer token and check the response headers for Keops-Gateway-Auth-Status, Keops-Decision, and Keops-Gateway-Auth-Duration
  • Logs: Check the Envoy sidecar logs (kubectl logs <pod> -c istio-proxy) for plugin execution entries (Access GRANTED, Access DENIED, Access SKIPPED)
  • Audit evidence: Verify the Access Gateway audit log contains the authorization check event

Troubleshooting​

  • 502 "Authorization Service Unavailable" — The plugin cannot reach the Access Gateway. Verify the EnvoyFilter defines a cluster named exactly access_gateway_cluster, which is the cluster the plugin always dispatches to. Check that the Access Gateway service is reachable from the mesh namespace.
  • Plugin not intercepting requests — Verify the WasmPlugin selector.matchLabels matches the target pod labels. Ensure Istio sidecar injection is enabled in the namespace.
  • Keops-Gateway-Auth-Status: SKIPPED on all requests — Check the skip_paths configuration. The matching uses prefix comparison, so / would match all paths.
  • WASM plugin not loading — Check the OCI image URL in the WasmPlugin resource. Verify the image is accessible from the cluster. Check the istio-proxy container logs for WASM loading errors.
  • Lua filter not applied — Verify the EnvoyFilter workloadSelector matches the target pod labels. Check the Envoy configuration to confirm the Lua filter is in the filter chain.
  • Timeout errors — Increase the timeout value and ensure the Envoy cluster connect_timeout matches. For cross-namespace calls, verify network policies allow traffic.
  • Lua decision headers missing on success — The Lua implementation uses Envoy dynamic metadata to pass headers from envoy_on_request to envoy_on_response. Verify both functions are included in the inline Lua code.

Next Steps​

To set up a local development environment for building and testing plugin changes, see Plugin Local Development & Test Setup.

For contributing changes back to the plugin repository, see Contributing to Enforcement Plugins.