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:
| Implementation | Language | Runtime | Best For |
|---|---|---|---|
| WASM | Go (compiled with TinyGo) | Envoy WASM VM | Production deployments, isolation, async processing |
| Lua | Lua (Envoy HTTP Lua filter) | Envoy Lua VM | Rapid 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:
- Captures the original request headers, method, path, and body
- Checks the path against a configurable skip list (for health checks, metrics endpoints)
- Removes protected headers to prevent spoofing (
Keops-Enforcer,Keops-Target-URI,Keops-Target-Method, andKeops-Client-Id, plus the olderKeymate-*spelling of each) - 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 - Dispatches an HTTP call to the Access Gateway via an Envoy cluster
- 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:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
gateway_url | String | Recommended | — | 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 |
endpoint | String | No | /auth | Authorization endpoint path appended to gateway_url |
timeout | Integer | No | 5000 | Request timeout in milliseconds |
cluster_name | String | No | — | 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_paths | String array | No | — | Request paths to skip authorization (for example, /health, /metrics) |
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:
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:
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:
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.
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.
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):
| Header | Value | Description |
|---|---|---|
Keops-Enforcer | layer="mesh", tech="istio", version="<plugin-version>" | Identifies the enforcer as an Istio mesh plugin |
Keops-Target-URI | Original request path | The target URI for access rule matching |
Keops-Target-Method | Original HTTP method | The HTTP method for resource resolution |
Keops-Client-Id | Client 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 |
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, andKeymate-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:
| Header | Source | Description |
|---|---|---|
Keops-Decision | Access Gateway | Authorization decision result and authority source |
Keops-Decision-Latency | Access Gateway | Total decision processing time in milliseconds |
Keops-Decision-Authority-Latency | Access Gateway | Authority call latency in milliseconds |
Keops-Decision-Cache | Access Gateway | Cache hit or miss indicator |
Keops-Gateway | Access Gateway | Access Gateway version |
Keops-Gateway-Auth-Duration | Plugin | Total plugin execution time in milliseconds |
Keops-Gateway-Auth-Status | Plugin | Plugin 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.
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
| Condition | HTTP Status | Response Body | Keops-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 service | GRANTED |
| Access Gateway returns 500, 502, 503 or 504 (WASM) | Same as gateway | Gateway response body | GATEWAY_FAILED |
| Access Gateway returns any other non-2xx status, such as 4xx (WASM) | Same as gateway | Gateway response body | DENIED |
| 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 check | SKIPPED |
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.
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
- The echo server receives the request (HTTP 200)
- The client response includes
Keops-Gateway-Auth-Status: GRANTEDandKeops-Decisionheaders - Requests to
/healthbypass authorization withKeops-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, andKeops-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.matchLabelsmatches the target pod labels. Ensure Istio sidecar injection is enabled in the namespace. Keops-Gateway-Auth-Status: SKIPPEDon all requests — Check theskip_pathsconfiguration. 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
workloadSelectormatches the target pod labels. Check the Envoy configuration to confirm the Lua filter is in the filter chain. - Timeout errors — Increase the
timeoutvalue and ensure the Envoy clusterconnect_timeoutmatches. 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_requesttoenvoy_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.
Related Docs
Extending the APISIX Plugin
Apache APISIX plugin architecture and configuration.
Plugin Local Development & Test Setup
Container and Kubernetes environment for plugin testing.
Access Gateway Overview
Endpoints, headers, and system role of the Access Gateway.
Enforcement Pipeline
How the Access Gateway processes authorization requests.