Istio Deep Dive — Why It Flows That Way
EnvoyFilter is a patch, not a translation
In one line
An EnvoyFilter is not a translation but a patch. After istiod has built the whole Envoy configuration from the other resources, you pick one spot in that result with applyTo, context and match, and insert or change a piece with operation. The value you insert is Envoy configuration as it is, so it can happen that Envoy rejects a patch that istioctl let through.
Why this was needed
VirtualService, DestinationRule and AuthorizationPolicy expose only some of what Envoy can do. Sometimes you need something outside that, such as local request limiting, Lua that handles a particular header, or a new filter the mesh has not yet wrapped in an API. Istio cannot create a new API for every Envoy feature, so it left an emergency exit for touching the generated configuration directly, and that is EnvoyFilter.
The emergency exit has a price. For other resources, istiod understands the meaning and takes responsibility for translating them, but the value of an EnvoyFilter is a lump of unknown meaning to istiod. istiod's translation result (filter names, order, shape) can change from version to version, and the patch leans on that result. That is why the official documentation warns at the very top that "if written wrongly, it can make the whole mesh unstable". In fact, istioctl 1.24.2 attaches the warning "EnvoyFilter exposes internal implementation details that may change at any time" even to a correct EnvoyFilter.
How it works
One patch is a pair of "where" and "what".
| Field | What it picks | Example |
|---|---|---|
applyTo |
The kind of Envoy object it touches | LISTENER · FILTER_CHAIN · NETWORK_FILTER · HTTP_FILTER · CLUSTER · ROUTE_CONFIGURATION |
match.context |
Which proxy and which direction | SIDECAR_INBOUND · SIDECAR_OUTBOUND · GATEWAY · ANY |
match.listener |
Which one within that kind | A port, filterChain.filter.name (a network filter), subFilter.name (an HTTP filter) |
match.proxy.proxyVersion |
Which version of proxy | A regular expression such as ^1\.24.* |
patch.operation |
How | INSERT_BEFORE · INSERT_AFTER · INSERT_FIRST · MERGE · REPLACE · REMOVE · ADD |
If you give HTTP_FILTER subFilter: envoy.filters.http.router and INSERT_BEFORE, the value goes as it is as one entry into the filter list of the inbound HTTP connection manager, right before router. So value is not Istio syntax but Envoy syntax. If you copy over a VirtualService's percentage: { value: 100 }, Envoy's fault filter (which uses FractionalPercent, with a numerator and a denominator) does not understand it.
There is a rule for order. EnvoyFilters in the root namespace (istio-system) are applied first and those in the workload namespace later, and if there are several on the same workload, they go in order of creation time. If they conflict with each other, the result is not defined. REPLACE does nothing if there is no target with a matching name — no error occurs, so it quietly drops out. The scope is decided by workloadSelector. If present, the workloads whose labels match; if absent, the whole namespace; if put in the root namespace, the whole mesh.
You also need to know how far the checking goes. istioctl checks the outer structure of an EnvoyFilter and tries to unpack the value into an Envoy type. If the @type name is missing altogether, it blocks with an error (rc=1), but a field that is not in that type gives only a warning and rc=0. And it knows nothing at all about Envoy's assembly rules, such as "router must be last in the list". The final verdict is Envoy's.
What it looks like in the field
I put in an EnvoyFilter and nothing changed. In production, if Envoy rejects the patched listener, the Pod keeps running fine and the old configuration stays (an xDS NACK). In istioctl proxy-status, only that proxy's configuration is out of line, and the reason for the rejection is printed in the istiod log. A patch that put a filter after router is the typical case. istioctl analyze giving an IST0151 warning for a relative-position operation with no priority is in the same vein — if the reference filter is not there yet, the patch is not applied.
CI is green but it breaks after deployment. If the pipeline looks only at the exit code of istioctl validate, a wrong field inside the value flows past as a warning. For EnvoyFilter alone, it is safer to treat warnings as failures, or to filter once more with envoy --mode validate an Envoy configuration that has the same value put in.
On upgrade day, half the mesh goes strange. If a filter name changes or the shape istiod makes changes, an old patch attaches to the wrong place or is rejected. If you tie versions together with proxyVersion, the patch is not attached to proxies of the new version, so instead of breaking, they come up without the feature. The standard way is to create and keep alongside a patch for the new version and move on.
Official documentation: EnvoyFilter · Envoy HTTP filters
What you will do in the next lab
You write an EnvoyFilter that puts a fault filter before router, set up by hand the result with the patch applied, and get a 418. You make one that puts the same filter after router and one that copies over a VirtualService-style percentage, and see twice the gap where istioctl lets it through but Envoy rejects it. Finally you check the version-tying regular expression against a real proxy version, and confirm the scope with a request-limiting filter that has no selector.