OPA vs Envoy WASM for Spatial Contract Validation
Choosing where spatial contract validation runs — an Open Policy Agent sidecar over ext_authz, or an Envoy WASM filter compiled into the proxy — decides your per-request latency budget, how expressive your geometry checks can be, and how much operational surface you carry. Both enforce the same intent: a payload crossing a domain boundary must satisfy its schema, declare EPSG:4326, and stay within a geometry-precision tolerance. But they make opposite trade-offs, and picking wrong shows up as either p99 latency creep or a validator that cannot do the numeric work spatial data demands. This decision page sits under Schema Contracts for Vector Tile Data, part of the federated routing architecture, and complements the format-level checks in Enforcing Schema Contracts for GeoJSON and Shapefiles; both approaches also serve the identity gate described in Zero-Trust Security for Spatial Endpoints.
Figure — Criterion-by-criterion comparison of OPA/Rego and an Envoy WASM filter for validating spatial data contracts at a domain boundary.
Prerequisites
| Requirement | Value / Constraint |
|---|---|
| Proxy / mesh | Envoy 1.28+ (WASM ABI v0.2.1) or Istio 1.20+; OPA 0.60+ for the ext_authz path |
| CLI tools | opa (for opa eval/opa test), istioctl, kubectl, curl, a WASM toolchain (tinygo or rustc + wasm32 target) |
| CRS assumption | Both validators enforce x-crs: EPSG:4326 for vector; a geometry-precision tolerance of 1e-6 or 1e-7 decimal degrees |
| Contract inputs | JSON Schema for feature collections; a geometry-precision bound; the pinned schema version, e.g. v1.2.0-crs:EPSG:4326-res:10m |
| Access roles | policy-author to publish Rego bundles / WASM modules; mesh-routing-admin to bind the filter |
| Env vars | OPA_BUNDLE_URL, WASM_MODULE_SHA, GEOM_PRECISION=1e-6 |
Both validators must be deterministic and idempotent: the same request yields the same allow/deny with no side effects, so retries under a routing retry storm never mutate state. Default-deny is the zero-trust baseline in either engine — a request that fails to parse is rejected, never admitted.
Decision Reference
| If you need… | Choose | Why |
|---|---|---|
| Sub-millisecond validation on the hot path | Envoy WASM | Runs in-process; no extra network hop or sidecar round-trip |
| Real numeric geometry work (precision, bbox containment, vertex counts) | Envoy WASM | Compiled code does floating-point math Rego cannot express cleanly |
| Policy authored and reviewed by non-C++ engineers | OPA / Rego | Declarative rules, unit-testable with opa test, readable in review |
| Live policy changes without redeploying the proxy | OPA / Rego | Push a new signed bundle; no proxy rebuild or restart |
| Decisions that consult external data (tenant residency, revocation lists) | OPA / Rego | Bundles carry reference data documents into the decision |
| Minimal moving parts and no extra service to run | Envoy WASM | Mesh-native filter; nothing else to deploy, scale, or monitor |
| A pragmatic default for most spatial contracts | Both, layered | WASM for cheap structural/geometry checks at the edge; OPA for policy that needs external context |
The common production answer is layered: a WASM filter does the cheap, high-frequency structural and geometry-precision checks in-process, and rejects fast; OPA handles the lower-frequency, context-rich policy (residency, scope claims, revocation) where expressiveness and hot-reload matter more than the extra hop.
Configuration Reference — OPA / Rego
The Rego module validates CRS and geometry precision and is testable offline with opa eval before it ever binds to the proxy.
package spatial.contract
import rego.v1
default allow := false
# Allow only EPSG:4326 vector payloads within the precision tolerance.
allow if {
input.attributes.request.http.headers["x-crs"] == "EPSG:4326"
prec := input.attributes.request.http.headers["x-geometry-precision"]
prec in ["1e-6", "1e-7"]
input.attributes.request.http.headers["x-schema-version"] == "v1.2.0-crs:EPSG:4326-res:10m"
}
# Structured diagnostic returned verbatim to the caller on denial.
violation contains msg if {
not allow
msg := sprintf("contract denied: crs=%v precision=%v", [
input.attributes.request.http.headers["x-crs"],
input.attributes.request.http.headers["x-geometry-precision"],
])
}
Test the decision without a running proxy, then publish it as a signed bundle:
# Evaluate the policy against a sample ext_authz input document.
opa eval -d spatial_contract.rego -i sample_input.json \
'data.spatial.contract.allow' --format pretty
Configuration Reference — Envoy WASM Filter
The WASM path compiles the same intent into a filter Envoy loads in-process. The EnvoyFilter sketch below pins the module by SHA-256 so a tampered binary is refused at load.
# spatial-contract-wasm.yaml — bind a WASM validator to the ingress listener
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: spatial-contract-validator
namespace: spatial
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter: { name: envoy.filters.network.http_connection_manager }
patch:
operation: INSERT_BEFORE
value:
name: spatial.contract
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: spatial_contract
root_id: spatial_contract
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: '{"crs":"EPSG:4326","precision":"1e-6"}'
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local: { filename: /etc/envoy/wasm/spatial_contract.wasm }
# Refuse to load a module whose hash does not match.
sha256: "${WASM_MODULE_SHA}"
Verify the filter is loaded and rejects a bad-CRS request in-process:
kubectl apply -f spatial-contract-wasm.yaml
curl -so /dev/null -w "%{http_code}\n" \
-H "x-crs: EPSG:3857" -H "x-geometry-precision: 1e-6" \
"https://spatial.mesh.enterprise.internal/v1/features/collections" # expect 403
Common Failure Modes & Fixes
- Symptom: p99 latency jumps ~3-8ms after enabling validation. Root cause: the OPA
ext_authzhop is on the synchronous hot path for every request. Fix: move high-frequency structural/geometry checks to the WASM filter and reserve OPA for context-rich policy; or co-locate the OPA sidecar to cut the round-trip. - Symptom: a geometry-precision rule is impossible to express in Rego without brittle string math. Root cause: Rego is set/JSON logic, not a numeric engine. Fix: implement precision, bbox-containment, and vertex-count checks in the WASM filter where floating-point compute is native; keep Rego for the allow/deny policy shape.
- Symptom: a policy change requires a proxy rebuild and rolling restart. Root cause: the logic lives in the compiled WASM module. Fix: for logic that changes often (residency lists, revocation), move it to an OPA bundle and push updates live; keep only stable structural checks in WASM.
- Symptom: WASM filter fails to load with a hash-mismatch error. Root cause: the deployed
.wasmbinary does not match the pinnedsha256. Fix: rebuild, recompute the digest, updateWASM_MODULE_SHA, and re-apply — never relax the pin, which is the zero-trust supply-chain guard. - Symptom: both validators disagree on the same payload. Root cause: the Rego and WASM implementations drifted (different precision constant or schema version). Fix: derive both from one contract manifest and add a conformance test that replays the same fixtures through
opa evaland the WASM module.
FAQ
Which is faster in practice for spatial contract checks?
Envoy WASM, by design. It runs inside the proxy with no extra network hop, so it typically adds well under a millisecond, whereas the OPA ext_authz pattern adds a synchronous round-trip to a separate process — a few milliseconds even when co-located. For a validation that fires on every request, that gap dominates the choice on latency-sensitive routes.
Can Rego do real geometry math like bbox containment or vertex counts?
Not cleanly. Rego excels at declarative set and JSON logic — is this header present, does this value belong to an allowed set, does the schema version match. Floating-point geometry work (precision tolerance, containment, ring vertex counts) is awkward and brittle in Rego and belongs in a compiled WASM filter. Use OPA for the policy decision, WASM for the numeric checks.
How do I change policy without redeploying the proxy?
Use OPA. Publish a new signed bundle to OPA_BUNDLE_URL and the sidecar hot-loads it — no proxy rebuild or restart. A WASM module, by contrast, is compiled into the filter, so any change means rebuild, re-pin the sha256, and roll the proxy. If your policy churns often, that alone argues for OPA.
Do these replace the GeoJSON/Shapefile ingest checks?
No — they complement them. The format-level structural gate in Enforcing Schema Contracts for GeoJSON and Shapefiles runs at ingest, while OPA or WASM validate the contract at the routing edge on every cross-domain hop. Identity and authorization for those hops are handled separately under Zero-Trust Security for Spatial Endpoints.
Is it wrong to run both OPA and WASM?
No — layering both is the common production answer. A WASM filter rejects malformed structure and bad geometry precision fast and in-process; OPA evaluates the policy that needs external context and frequent change. Derive both from one contract manifest and add a conformance test so they never disagree.
Related
- Up to the parent: Schema Contracts for Vector Tile Data — the contract-first model both engines enforce
- Enforcing Schema Contracts for GeoJSON and Shapefiles — the ingest-time structural gate these validators complement
- Zero-Trust Security for Spatial Endpoints — the identity and mTLS layer alongside contract validation
- Cross-Domain Routing Strategies — the routing edge where these validators run on every hop
- Federated Ownership & Routing Architecture — the parent reference establishing policy-as-code at domain boundaries