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 (
Keymate-Enforcer,Keymate-Target-URI,Keymate-Target-Method) - Adds Keymate-specific headers identifying the mesh enforcer, target URI, and HTTP method
- Dispatches an HTTP call to the Access Gateway via an Envoy cluster
- Interprets the response: 2xx means allow, anything else means deny
2. Understand the configuration
The plugin accepts the following configuration parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
gateway_url | String | Yes | — | Base URL of the Access Gateway instance |
endpoint | String | No | /auth | Authorization endpoint path appended to gateway_url |
timeout | Integer | No | 5000 | Request timeout in milliseconds |
cluster_name | String | No | Extracted from gateway_url host | Envoy cluster name for routing to the Access Gateway |
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: keymate-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
cluster_name: "access_gateway_cluster"
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/keymate-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 |
|---|---|---|
Keymate-Enforcer | layer="mesh", tech="istio", version="<plugin-version>" | Identifies the enforcer as an Istio mesh plugin |
Keymate-Target-URI | Original request path | The target URI for access rule matching |
Keymate-Target-Method | Original HTTP method | The HTTP method for resource resolution |
Headers filtered (not forwarded to Access Gateway):
The plugin removes these headers from the forwarded request to prevent client-side spoofing:
Keymate-Enforcer,Keymate-Target-URI,Keymate-Target-Method— always set by the plugin- 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 |
|---|---|---|
Keymate-Decision | Access Gateway | Authorization decision result and authority source |
Keymate-Decision-Latency | Access Gateway | Total decision processing time in milliseconds |
Keymate-Decision-Authority-Latency | Access Gateway | Authority call latency in milliseconds |
Keymate-Decision-Cache | Access Gateway | Cache hit or miss indicator |
Keymate-Gateway | Access Gateway | Access Gateway version |
Keymate-Gateway-Auth-Duration | Plugin | Total plugin execution time in milliseconds |
Keymate-Gateway-Auth-Status | Plugin | Plugin outcome: GRANTED, DENIED, SKIPPED, or GATEWAY_FAILED |
The Keymate-Gateway-Auth-Status header is unique to the Istio plugin. It provides an explicit status label (GRANTED, DENIED, SKIPPED, GATEWAY_FAILED) that is useful for observability dashboards and log aggregation. The SKIPPED status is set when the request path matches a skip_paths entry. The APISIX plugin also uses Keymate-Gateway-Auth-Status but does not support the SKIPPED value.
6. Understand error handling
| Condition | HTTP Status | Response Body | Keymate-Gateway-Auth-Status |
|---|---|---|---|
| Access Gateway unreachable | 502 | {"error": "Authorization Service Unavailable (Dispatch Failed)"} | GATEWAY_FAILED |
| Access Gateway timeout | 502 | {"error": "Authorization Service Unavailable (No Headers)"} | GATEWAY_FAILED |
| Access Gateway returns 2xx | — | Request forwarded to service | GRANTED |
| Access Gateway returns 4xx | Same as gateway | Gateway response body | DENIED |
| Access Gateway returns 5xx | Same as gateway | Gateway response body | GATEWAY_FAILED |
| Path in skip list | — | Request forwarded without auth check | SKIPPED |
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 Keymate-* 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
Keymate-Gateway-Auth-Status: GRANTEDandKeymate-Decisionheaders - Requests to
/healthbypass authorization withKeymate-Gateway-Auth-Status: SKIPPED
How to Verify
- API evidence: Send a request with a valid Bearer token and check the response headers for
Keymate-Gateway-Auth-Status,Keymate-Decision, andKeymate-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
cluster_namematches the EnvoyFilter cluster definition. 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. Keymate-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.