Configuring Raw Envoy Filters in Istio with EnvoyFilter
For the features Istio's own APIs don't expose, EnvoyFilter patches the underlying Envoy configuration directly — carefully.
Istio’s high-level resources, VirtualService, DestinationRule, AuthorizationPolicy, cover most day-to-day traffic management without anyone needing to know Envoy’s configuration format directly. But Envoy has far more filters and options than any of those resources expose, and for the gap, Istio provides EnvoyFilter: a resource that patches the Envoy configuration Istio generates, directly. It is a genuine escape hatch, powerful and easy to misconfigure in equal measure, and this guide covers how it’s structured, with a complete worked example.
What EnvoyFilter is for
If what you need has a dedicated Istio resource, a routing rule, a retry policy, mutual TLS settings, use that resource instead. EnvoyFilter exists for everything else: filters like Lua scripting, request tapping, IP tagging, or any other Envoy capability that Istio’s higher-level APIs simply don’t have a field for. Reach for it only once you’ve confirmed the thing you need genuinely isn’t expressible through the normal resources, since every EnvoyFilter you add is one more thing to keep in sync with however Istio’s own generated configuration evolves across upgrades.
The four questions every EnvoyFilter answers
Every EnvoyFilter resource, regardless of what it’s configuring, answers the same four questions:
- Which proxies does this apply to? A
workloadSelectortargets specific workloads by label; omitting it applies the patch mesh-wide within its namespace, or mesh-wide if placed in Istio’s root/config namespace. - Where in the Envoy configuration does the patch apply? The
applyTofield names the target:LISTENER,FILTER_CHAIN,HTTP_FILTER,NETWORK_FILTER,CLUSTER,ROUTE_CONFIGURATION, and a handful of others. - More specifically, which instance of that thing? A
matchblock narrows it further, for example a specific listener port, a specific filter chain, or whether this applies to sidecar inbound traffic, sidecar outbound traffic, or a gateway. - What operation should be performed, and with what content? The
patchblock specifies both an operation,ADD,REMOVE,MERGE,INSERT_BEFORE,INSERT_AFTER, orINSERT_FIRST, and, for anything that inserts or merges configuration, the actual filter configuration as itsvalue.
Order-sensitive changes (inserting an HTTP filter at a specific point in the chain) use the INSERT_* operations and point at a named neighbor, typically the terminal envoy.filters.http.router filter, to say “before this” or “after this.”
A complete example: tagging requests by IP
This adds the ip_tagging HTTP filter to sidecar-inbound traffic on port 80, inserted just before the terminal router filter:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: ip-tagging
namespace: default
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
portNumber: 80
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
subFilter:
name: envoy.filters.http.router
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.ip_tagging
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging
request_type: BOTH
ip_tags:
- ip_tag_name: hello-envoy-conf
ip_list:
- address_prefix: "0.0.0.0"
prefix_len: 0
Reading it top to bottom: no workloadSelector means this applies to every workload in the default namespace. applyTo: HTTP_FILTER says we’re adding an HTTP filter. The match block narrows that to sidecar-inbound traffic, on the listener bound to port 80, specifically within the HTTP Connection Manager’s filter chain, positioned relative to the router filter. The patch says to insert a new filter before that router, and gives its full configuration: the filter’s name, its type URL (@type), and the filter-specific fields, here, tagging every request with X-Envoy-IP-Tags: hello-envoy-conf regardless of source IP (0.0.0.0/0 matches everything).
Apply it and confirm:
kubectl apply -f ip-tagging-filter.yaml
kubectl exec deploy/httpbin -c istio-proxy -- curl -s -D - http://localhost:80/get -o /dev/null | grep -i x-envoy-ip-tags
Finding the name, type URL, and fields for a filter
The name, typed_config’s @type, and the fields that go under it are all specific to each filter, and Envoy’s own filter documentation is where they come from, though it’s spread across several linked pages rather than laid out in one place: a filter’s overview page usually links out to its protobuf reference, which in turn defines every field, often with further links for nested types. Budget for a fair amount of tab-switching. A few things make it faster:
- Search for the type URL on GitHub once you have it. For any reasonably common filter, someone has likely already published a working
EnvoyFilteror raw Envoy config using it, which is usually a faster starting point than assembling one from the field reference alone. - Not every Envoy filter is compiled into Istio’s proxy build. Istio ships a specific Envoy build, and a filter documented on envoyproxy.io isn’t guaranteed to be included in it. Confirm a filter is actually available in Istio’s build before designing a solution around it.
- Use JSON instead of YAML for the
valueblock if you find YAML’s indentation error-prone for deeply nested configuration; Envoy’s configuration accepts either, and a misplaced indent in a nested YAML block is one of the most common ways anEnvoyFiltersilently fails to do what you intended.
Inspecting what’s actually running
Two tools help confirm a patch landed the way you expect, rather than guessing from behavior alone:
istioctl proxy-config listener POD_NAME -n NAMESPACE
istioctl proxy-config route POD_NAME -n NAMESPACE
istioctl proxy-config cluster POD_NAME -n NAMESPACE
istioctl proxy-config all POD_NAME -n NAMESPACE -o json
istioctl proxy-config, followed by bootstrap, clusters, routes, listeners, or all, prints a given sidecar’s actual configuration as Istio and Envoy have assembled it, as a table by default or as YAML/JSON with -o. This is the ground truth for what’s really running, as opposed to what you intended to patch. For visually comparing a full config dump, a full Envoy configuration is more than most people want to read directly in a terminal; a config-dump viewer that renders it as a browsable, cross-linked structure (Envoy’s community maintains a few) is usually easier to work through than scrolling raw JSON.
The core caution
EnvoyFilter patches configuration Istio itself generates and regenerates on every change, which means it’s inherently coupled to Istio’s internals in a way the higher-level resources aren’t. An EnvoyFilter written against one Istio version can silently stop matching anything, or match something unintended, after an upgrade changes how Istio structures its generated listeners or filter chains. Treat it the way the name suggests: a precise tool for a specific, verified gap, tested after every Istio upgrade, not a default way to configure traffic.
Popular Guides
-
Envoy Architecture: Listeners, Clusters, and Filter Chains How a request actually moves through Envoy, from the socket it arrives on to the upstream it's sent to. -
Where Envoy Fits: Edge, Front Proxy, and Service Mesh Patterns The same proxy, deployed three different ways — and why 'lightweight enough to run per instance' is what makes all three possible. -
The HTTP Connection Manager and HTTP Filter Chains How Envoy turns raw bytes into HTTP requests it can act on, and the ways you can plug your own logic into that pipeline.