Announcing Built On Envoy: Making Envoy Extensions Accessible to Everyone

Learn more

Header-Based Routing in Istio without Header Propagation

Istio uses Envoy proxy as a Pod sidecar to which the application delegates networking responsibilities like the inbound and outbound traffic

Header-Based%20Routing%20in%20Istio%20without%20Header%20Propagation

Istio uses Envoy proxy as a Pod sidecar to which the application delegates networking responsibilities like the inbound and outbound traffic, but there’s one responsibility that still belongs to the app container: header propagation.

The Envoy proxy cannot correlate the requests it sends to the app to those the app is responding to, so the headers cannot be automatically propagated by Istio.

Post Image
Figure 1: Figure 1: Sidecar can’t correlate requests with responses if the app container does not forward back headers.

In most cases, headers-based routing would require app developers to implement header forwarding. For example, in Istio’s flagship Bookinfo app, the productpage microservice implements it like this. This drives us to a question:

How can platform admins use headers-based routing without modifying the application’s internals?

## A Swim-Lane Approach

Using the Bookinfo app, we’ll segment request paths based on an x-version header as in Figure 2 below:

Post Image
Figure 2: Figure 2: Segmenting request paths based on x-version header.

Requests with no x-version header may be routed to an arbitrary backend.

Deploy Workloads

We will use Istio’s Bookinfo example with some minor changes regarding versioning apps as a sample implementation.

First, we create three productpage deployments, only changing the version labels.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: productpage-v{1,2,3}
  labels:
    app: productpage
    version: v{1,2,3}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: productpage
      version: v{1,2,3}
  template:
    metadata:
      labels:
        app: productpage
        version: v{1,2,3}
...

And one service for them all:

apiVersion: v1
kind: Service
metadata:
  name: productpage
  labels:
    app: productpage
    service: productpage
spec:
  ports:
  - port: 9080
    name: http
  selector:
    app: productpage

Then, create three deployments for the reviews app:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v{1,2,3}
  labels:
    app: reviews
    version: v{1,2,3}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reviews
      version: v{1,2,3}
  template:
    metadata:
      labels:
        app: reviews
        version: v{1,2,3}
...

Ratings and details apps just keep the same as in the original example.

Deploy Istio Config

Here’s where Istio’s routing capabilities come into play. A DestinationRule subsetsfor each productpage version is defined:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: productpage
spec:
  host: productpage
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3

And a couple of VirtualService that implement the first half of the swim-lane headers logic. The following takes charge of the prefix matches and uses the delegate functionality to use a second VirtualService, so configs are atomic and declaring a mesh gateway selector is avoided (see quote below):

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
  - "*"
  gateways:
  - bookinfo-gateway
  http:
  - match:
    - uri:
        exact: /productpage
    - uri:
        prefix: /static
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products
    delegate:
      name: productpage-route

Now the delegated productpage-route:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: productpage-route
spec:
  http:
  - name: "productpage-v1-route"
    match:
    - headers:
        x-version:
          exact: v1
    route:
    - destination:
        host: productpage
        subset: v1
  - name: "productpage-v2-route"
    match:
    - headers:
        x-version:
          exact: v2
    route:
    - destination:
        host: productpage
        subset: v2
  - name: "productpage-v3-route"
    match:
    - headers:
        x-version:
          exact: v3
    route:
    - destination:
        host: productpage
        subset: v3
  - name: "productpage-default-route"
    match:
    - withoutHeaders:
        x-version: {}
    route:
    - destination:
        host: productpage

Then, at the Reviews level, craft the second half of the swim-lane using the sourceLabels config at the httpMatchRequest:

One or more labels that constrain the applicability of a rule to source (client) workloads with the given labels. If the VirtualService has a list of gateways specified in the top-level gateways field, it must include the reserved gateway mesh for this field to be applicable.

Source: Istio Virtual Service Documentation

This is the VirtualService using the sourceLabels feature:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews-route
spec:
  hosts:
  - reviews
  http:
  - name: "reviews-v1-route"
    match:
    - sourceLabels:
        version: v1
    route:
    - destination:
        host: reviews
        subset: v1
  - name: "reviews-v2-route"
    match:
    - sourceLabels:
        version: v2
    route:
    - destination:
        host: reviews
        subset: v2
  - name: "reviews-v3-route"
    match:
    - sourceLabels:
        version: v3
    route:
    - destination:
        host: reviews
        subset: v3

Testing Header Routing without Header Propagation

First, start with the no header scenario, where you get responses from all lanes:

—————»  ns:bookinfo ❯ for i in {1..5}; do curl -s localhost:8080/productpage | grep -A1 "Reviews served by"; done
        <dt>Reviews served by:</dt>
        <u>reviews-v2-955b74755-t4jkb</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v2-955b74755-t4jkb</u>

Then check if the x-version: v1 header has any effect. You can see all the calls productpage-v1 workload make are being served exclusively by reviews-v1.

—————»  ns:bookinfo ❯ for i in {1..10}; \
do curl -s localhost:8080/productpage -H "x-version: v1" \
| grep -A1 "Reviews served by"; done
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v1-5cf854487-hjtrg</u>

And finish testing with v3 header value:

—————»  ns:bookinfo ❯ for i in {1..10}; \
do curl -s localhost:8080/productpage -H "x-version: v3" \
| grep -A1 "Reviews served by"; done
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>
        <dt>Reviews served by:</dt>
        <u>reviews-v3-797fc48bc9-wsg26</u>

Conclusion

In this article we used match on headers, subsets and sourceLabels in Istio to route based on headers with no header propagation. You could also see the usage of delegate functionality as well as withoutHeaders matching.

Product background Product background for tablets
Building AI agents

Agent Router Enterprise provides managed LLM & MCP Gateways plus AI Guardrails in your dedicated instance. Graduate agents from prototype to production with consistent model access, governed tool use, and runtime supervision — built on Envoy AI Gateway by its creators.

  • LLM Gateway – Unified model catalog with automatic fallback across providers
  • MCP Gateway – Curated tool access with per-profile authentication and filtering
  • AI Guardrails – Enforce policies, prevent data loss, and supervise agent behavior
  • Learn more
    Replacing NGINX Ingress

    Tetrate Enterprise Gateway for Envoy (TEG) is the enterprise-ready replacement for NGINX Ingress Controller. Built on Envoy Gateway and the Kubernetes Gateway API, TEG delivers advanced traffic management, security, and observability without vendor lock-in.

  • 100% upstream Envoy Gateway – CVE-protected builds
  • Kubernetes Gateway API native – Modern, portable, and extensible ingress
  • Enterprise-grade support – 24/7 production support from Envoy experts
  • Learn more
    Decorative CTA background pattern background background
    Tetrate logo in the CTA section Tetrate logo in the CTA section for mobile

    Ready to enhance your
    network

    with more
    intelligence?