Envoy Envoy

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.

14 min Advanced

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:

  1. Which proxies does this apply to? A workloadSelector targets 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.
  2. Where in the Envoy configuration does the patch apply? The applyTo field names the target: LISTENER, FILTER_CHAIN, HTTP_FILTER, NETWORK_FILTER, CLUSTER, ROUTE_CONFIGURATION, and a handful of others.
  3. More specifically, which instance of that thing? A match block 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.
  4. What operation should be performed, and with what content? The patch block specifies both an operation, ADD, REMOVE, MERGE, INSERT_BEFORE, INSERT_AFTER, or INSERT_FIRST, and, for anything that inserts or merges configuration, the actual filter configuration as its value.

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 EnvoyFilter or 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 value block 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 an EnvoyFilter silently 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