Istio’s power comes from Envoy’s extensibility, and EnvoyFilter is your direct line to that power.
Let’s see how we can inject custom Envoy configuration into a service running within Istio. Imagine we have a service my-service in the default namespace, and we want to add a custom HTTP filter that logs every request’s x-request-id header to stdout.
Here’s the EnvoyFilter that accomplishes this:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: custom-request-id-logger
namespace: default
spec:
workloadSelector:
labels:
app: my-service
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
subFilter:
name: "envoy.filters.http.router"
patch:
operation: INSERT_BEFORE
value:
name: "envoy.filters.http.lua"
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
local req_id = request_handle:headers():get("x-request-id")
if req_id then
request_handle:logInfo("Request ID: " .. req_id:string())
end
end
When this EnvoyFilter is applied, Istio injects a Lua filter before the router filter in the inbound HTTP connection manager for pods labeled app: my-service. The Lua code simply retrieves the x-request-id header and logs it.
Let’s break down what’s happening:
workloadSelector: This targets specific pods. Here, it’s pods in thedefaultnamespace with the labelapp: my-service. Istio will apply this filter only to the Envoy sidecars associated with these pods.configPatches: This is where the magic happens. You define a list of patches to apply to the Envoy configuration.applyTo: HTTP_FILTER: We’re modifying an HTTP filter.match: This specifies where in the Envoy configuration the patch should be applied.context: SIDECAR_INBOUND: We’re targeting the inbound traffic handled by the sidecar before it reaches the application container. You could also useSIDECAR_OUTBOUNDfor outbound traffic.listener: We’re looking at a specific listener.filterChain: Within the listener, we’re interested in a particular filter chain.filter: We’re targeting thehttp_connection_managerfilter, which is fundamental to HTTP traffic processing in Envoy.subFilter: Within thehttp_connection_manager, we’re targeting therouterfilter. This is a common place to insert custom logic because the router is typically one of the last filters in the chain, meaning most request processing has already occurred, and headers are readily available.
patch: This defines the actual modification.operation: INSERT_BEFORE: We want to insert our new filter before the matchedrouterfilter. Other operations includeINSERT_AFTER,REMOVE, andMERGE.value: This is the actual Envoy configuration snippet (in YAML, which gets translated to Protobuf internally) for the filter we want to insert.name: "envoy.filters.http.lua": This is the name of the Envoy HTTP filter we’re adding. Envoy has a rich set of built-in filters.typed_config: This contains the specific configuration for the Lua filter.inline_code: This is where you embed the Lua script that Envoy will execute.
The beauty of EnvoyFilter is that it allows you to precisely target where and how you modify Envoy’s configuration without needing to rebuild or reconfigure the entire Istio control plane. You can add custom authentication logic, modify headers, implement rate limiting beyond Istio’s defaults, or even perform complex request transformations.
When you apply this EnvoyFilter, Istio’s agent (istiod) will detect the change and dynamically reconfigure the Envoy proxy running as a sidecar for my-service. The proxy will reload its configuration, adding your Lua filter into the HTTP filter chain. You’ll then see the Request ID: ... log messages appearing in the my-service pod’s sidecar logs.
The most surprising thing about EnvoyFilter is that it doesn’t just add new filters; it can also modify existing ones, merge their configurations, or remove them entirely, giving you granular control over the entire Envoy request processing pipeline. For instance, you could use MERGE to add specific typed_per_filter_config to an existing filter.
Now that you’ve seen how to add a simple Lua filter, the next logical step is to explore how to modify existing filters or even inject entirely new network-level filters.