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 (Keymate-Enforcer, Keymate-Target-URI, Keymate-Target-Method)
  4. Adds Keymate-specific headers identifying the mesh enforcer, target URI, and HTTP method
  5. Dispatches an HTTP call to the Access Gateway via an Envoy cluster
  6. Interprets the response: 2xx means allow, anything else means deny

2. Understand the configuration

The plugin accepts the following configuration parameters:

ParameterTypeRequiredDefaultDescription
gateway_urlStringYesBase URL of the Access Gateway instance
endpointStringNo/authAuthorization endpoint path appended to gateway_url
timeoutIntegerNo5000Request timeout in milliseconds
cluster_nameStringNoExtracted from gateway_url hostEnvoy cluster name for routing to the Access Gateway
skip_pathsString arrayNoRequest 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: 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:

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/keymate-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
Keymate-Enforcerlayer="mesh", tech="istio", version="<plugin-version>"Identifies the enforcer as an Istio mesh plugin
Keymate-Target-URIOriginal request pathThe target URI for access rule matching
Keymate-Target-MethodOriginal HTTP methodThe 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:

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

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

ConditionHTTP StatusResponse BodyKeymate-Gateway-Auth-Status
Access Gateway unreachable502{"error": "Authorization Service Unavailable (Dispatch Failed)"}GATEWAY_FAILED
Access Gateway timeout502{"error": "Authorization Service Unavailable (No Headers)"}GATEWAY_FAILED
Access Gateway returns 2xxRequest forwarded to serviceGRANTED
Access Gateway returns 4xxSame as gatewayGateway response bodyDENIED
Access Gateway returns 5xxSame as gatewayGateway response bodyGATEWAY_FAILED
Path in skip listRequest forwarded without auth checkSKIPPED

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.

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 Keymate-Gateway-Auth-Status: GRANTED and Keymate-Decision headers
  3. Requests to /health bypass authorization with Keymate-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, and Keymate-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_name matches the EnvoyFilter cluster definition. 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.
  • Keymate-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.