What Replaces rewrite-target in Envoy Gateway?

Tested on RHEL 10.2 (Coughlan) workstation
Package kubectl 1.37.0 (server 1.33.1)
ingress-nginx controller v1.15.1 (chart 4.15.1)
Envoy Gateway v1.9.1
Gateway API CRDs v1.6.1 (experimental channel)
ingress2gateway 1.0.0
Applies to Any host with kubectl configured; any Kubernetes cluster that already runs ingress-nginx and Envoy Gateway
Lab environment Both controllers already installed, with a GatewayClass named eg
Privilege Normal user
Scope Why rewrite-target and regex paths do not always send the same path to the backend Service on Envoy Gateway, how a match differs from a rewrite, and how to replace a prefix or expand a capture group. Does not cover installing the controllers, authentication, or header and redirect annotations.
Related guides Migrate ingress-nginx to Envoy Gateway
Kubernetes Gateway API
Kubernetes Ingress rules
Kubernetes Services
Short on time?Beta Get the gist of this 15-minute read in seconds, then ask follow-up questions

In ingress-nginx, nginx.ingress.kubernetes.io/rewrite-target changes the URL path that nginx sends to the backend Service. It does not decide which backend should receive the request; the Ingress path does that.

Path matching and path rewriting solve different problems. A match decides whether /files/hostname belongs to this route. A rewrite decides whether the backend receives /files/hostname, /hostname, /, or something else.

That distinction matters during migration because a route can match successfully and still send the wrong path to the application. An HTTP 200 from Envoy is not enough to prove the rewrite is correct. The backend may successfully answer the wrong path.

The examples reuse the setup from the ingress-nginx to Envoy Gateway migration. ingress-nginx is reachable on 127.0.0.1:18080, and Envoy Gateway is reachable on 127.0.0.1:19080.

The web Service returns the request path it receives as its response body. That makes a bad rewrite easy to see: if the backend receives /$2, the response body is literally /$2.

The generated Gateway is named nginx, but its gatewayClassName must be changed to eg before Envoy Gateway will manage it.

     Client requests /files/hostname
                    ↓
HTTPRoute match accepts or rejects the path
                    ↓
Rewrite sets the path the backend receives
                    ↓
       Service web returns that path

What rewrite-target sends to the backend

The two common rewrite patterns are a fixed target and a regex rewrite that keeps part of the original path. One sends /shop/... to /. The other sends /files/... to /.... The Ingress path decides whether the request matches this rule. rewrite-target decides which path is sent to the backend after that match.

Ingress rule Request Backend receives
path: /shop and rewrite-target: / /shop/hostname /
regex and rewrite-target: /$2 /files/hostname /hostname
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: prefix-rewrite
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: shop.example.test
    http:
      paths:
      - path: /shop
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: regex-rewrite
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: files.example.test
    http:
      paths:
      - path: /files(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: web
            port:
              number: 8080

Request /shop/hostname on the prefix Ingress. The body is the path web received:

bash
curl -sS --resolve shop.example.test:18080:127.0.0.1 http://shop.example.test:18080/shop/hostname
output
/

rewrite-target: / replaced the whole path. The suffix hostname is gone. From ingress-nginx 0.22 onward that is the normal behavior unless the target uses a capture group.

The regex Ingress keeps the part after /files/. /$2 puts that remainder back on a leading slash.

A capture group is the part of a regular expression enclosed in parentheses. nginx can reuse the text matched by those groups in rewrite-target as $1, $2, and so on:

  • Group 1 is the slash, or the end of the path
  • Group 2 is the remainder
bash
curl -sS --resolve files.example.test:18080:127.0.0.1 http://files.example.test:18080/files/hostname
output
/hostname

Normally, a Kubernetes Prefix path /shop does not match /shopping. Prefix matching is element by element. /shop matches these paths:

  • /shop
  • /shop/
  • /shop/hostname

It does not match /shopping, because shopping is not a child of shop. In this ingress-nginx configuration, rewrite-target causes the controller to render that host's paths as case-insensitive regular-expression locations. /shopping reaches this route for that reason, and the rewrite still replaces the entire path:

bash
curl -sS --resolve shop.example.test:18080:127.0.0.1 http://shop.example.test:18080/shopping
output
/

The body is /, so /shopping was accepted by this route and then rewritten away.

These paths are also case-insensitive in this ingress-nginx configuration. If rewrite-target or use-regex is used for a host, ingress-nginx applies case-insensitive regex matching to all paths for that host:

  • /Shop/hostname matches the same route as /shop/hostname
  • /Files/hostname matches the same route as /files/hostname
  • /other/hostname matches neither rule, so ingress-nginx returns its default-backend 404

A match is not a rewrite

Keep the two steps separate when you read a generated HTTPRoute.

  • The route match decides which requests enter the rule. That includes the path type, the path value, and, for regular expressions, details such as case-insensitive matching with (?i).
  • The rewrite runs only after a match. It sets the path the backend Service sees.
  • A route can match and still send the wrong path. That is what a literal /$2 looks like.

Core Gateway API gives you two path rewrites on an HTTPRoute URLRewrite filter. For a request to /shop/hostname:

Rewrite Replacement Backend receives
ReplaceFullPath / /
ReplacePrefixMatch / /hostname

What each field does:

  • ReplaceFullPath sets one fixed path. It does not expand $1 or $2.
  • ReplacePrefixMatch replaces only the matched prefix and preserves the remaining path.
  • Neither performs regex capture-group substitution. Capture groups need an Envoy Gateway HTTPRouteFilter.

HTTPRouteFilter is an Envoy Gateway-specific extension, not a core Gateway API resource. An HTTPRoute attaches it through an ExtensionRef filter.

ReplacePrefixMatch / on /shop/hostname works like this:

   /shop/hostname
          ↓
ReplacePrefixMatch: /
          ↓
      /hostname

What ingress2gateway prints for these paths

Confirm the binary before you read the YAML. The 1.0.0 release I used still prints dev:

bash
ingress2gateway version
output
ingress2gateway version: dev
Built with Go version: go1.25.8

Print the Gateway API objects for the shop namespace. The important part is the generated rewrite for the regex Ingress. Instead of expanding $2, ingress2gateway 1.0.0 writes it as a fixed ReplaceFullPath value. Warnings go to stderr:

bash
ingress2gateway print --providers=ingress-nginx --namespace=shop --no-color
output
┌─ WARN  ────────────────────────────────────────
│  Ingress NGINX serves TLS traffic for host "files.example.test" with a self-signed certificate. This behavior will not be translated and the host will not be accessible via HTTPS.
│  source: INGRESS-NGINX
└─
┌─ WARN  ────────────────────────────────────────
│  Ingress NGINX serves TLS traffic for host "shop.example.test" with a self-signed certificate. This behavior will not be translated and the host will not be accessible via HTTPS.
│  source: INGRESS-NGINX
└─
┌─ INFO  ────────────────────────────────────────
│  Using case-insensitive regex path matches. You may want to change this.
│  source: INGRESS-NGINX
│  object: HTTPRoute: shop/regex-rewrite-files-example-test
└─
┌─ INFO  ────────────────────────────────────────
│  Using case-insensitive regex path matches. You may want to change this.
│  source: INGRESS-NGINX
│  object: HTTPRoute: shop/prefix-rewrite-shop-example-test
└─
┌─ WARN  ────────────────────────────────────────
│  Gateway API does not support configuring URL normalization (RFC 3986, Section 6). Please check if this matters for your use case and consult implementation-specific details.
│  source: STANDARD_EMITTER
└─

Do not ignore these messages. They reveal behavior that may change during the migration.

  • The TLS warning appears even though these Ingresses have no tls block. ingress-nginx can still serve its default certificate. This page stays on HTTP.
  • Both routes, including the prefix that never set use-regex, are case-insensitive regular expressions. The INFO line is telling you to review that.
  • Nothing in this report mentions $2.

There is an important difference between the documented behavior and the 1.0.0 release binary:

  • The provider documentation says capture-group references such as $2 are unsupported and should be reported.
  • In this test, 1.0.0 generated replaceFullPath: /$2 without a capture-group warning.

Verify the generated route itself, not only the warning report.

This exact warning behavior is specific to the 1.0.0 binary I tested. ingress2gateway 1.2.0 includes a fix for regex-rewrite warnings, so newer versions may report this case differently. The provider documentation still says capture-group references should be flagged. Review the warnings from the version you run.

I also ran the same print with --emitter=envoy-gateway. The HTTPRoutes were identical. The Envoy emitter did not add an HTTPRouteFilter.

The generated Gateway uses gatewayClassName: nginx. Change that to eg or Envoy will not program the routes. These are the two HTTPRoutes, with the empty status blocks removed:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  annotations:
    gateway.networking.k8s.io/generator: ingress2gateway-dev
  name: prefix-rewrite-shop-example-test
  namespace: shop
spec:
  hostnames:
  - shop.example.test
  parentRefs:
  - name: nginx
  rules:
  - backendRefs:
    - name: web
      port: 8080
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          replaceFullPath: /
          type: ReplaceFullPath
    matches:
    - path:
        type: RegularExpression
        value: (?i)/shop.*
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  annotations:
    gateway.networking.k8s.io/generator: ingress2gateway-dev
  name: regex-rewrite-files-example-test
  namespace: shop
spec:
  hostnames:
  - files.example.test
  parentRefs:
  - name: nginx
  rules:
  - backendRefs:
    - name: web
      port: 8080
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          replaceFullPath: /$2
          type: ReplaceFullPath
    matches:
    - path:
        type: RegularExpression
        value: (?i)/files(/|$)(.*).*

The fixed rewrite preserves the original nginx behavior. /shop/hostname still arrives as /. The capture-group rewrite does not: ingress2gateway generated ReplaceFullPath: /$2, so Envoy sends the literal string /$2 upstream. Request that URL through Envoy while the generated route is in place:

bash
curl -sS --resolve files.example.test:19080:127.0.0.1 http://files.example.test:19080/files/hostname
output
/$2

The status is 200. The backend received the characters $2, not the captured suffix. ReplaceFullPath is a fixed string. Envoy does not treat $2 as an nginx capture.

The generated shop expression (?i)/shop.* also matches /shopping and replaces the whole path, so Envoy returns / for that URL, the same body ingress-nginx returned.


Keep the path suffix with ReplacePrefixMatch

Do not use ReplacePrefixMatch merely because you are migrating rewrite-target: /. It changes the behavior.

The difference is easiest to see with the same request, /shop/hostname:

  • Original rewrite-target: / sends /
  • ReplacePrefixMatch: / sends /hostname

Use ReplacePrefixMatch when the application expects that suffix. Use ReplaceFullPath / when you want the original rewrite-target: / result.

Gateway API PathPrefix matches element by element. /shop matches /shop and /shop/hostname. It does not match /shopping.

Replace the generated shop route with this HTTPRoute. Apply it in the next section, then run the shop checks. That file contains:

  • this prefix HTTPRoute
  • the capture-group HTTPRouteFilter
  • the files HTTPRoute
yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: prefix-rewrite-shop-example-test
  namespace: shop
spec:
  hostnames:
  - shop.example.test
  parentRefs:
  - name: nginx
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /shop
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /
    backendRefs:
    - name: web
      port: 8080

Expand capture groups with an Envoy filter

Core Gateway API URLRewrite supports ReplaceFullPath and ReplacePrefixMatch, but it does not support regex capture-group substitution. Envoy Gateway adds that capability through HTTPRouteFilter and ReplaceRegexMatch:

  • Core fields: ReplaceFullPath and ReplacePrefixMatch
  • Envoy extension: ReplaceRegexMatch on HTTPRouteFilter (gateway.envoyproxy.io/v1alpha1)
  • Pattern language: RE2
         ingress-nginx rewrite-target: /$2
                         ↓
HTTPRouteFilter ReplaceRegexMatch, substitution /\1

The two regex patterns use different capture groups, so the group number changes:

  • In the original nginx regex, group 1 is (/|$), the slash or the end of the path
  • In the original nginx regex, group 2 is the remainder, so the target is $2
  • In the new Envoy regex, only the remainder is captured, so it is group 1 and the substitution is \1
  • That is not a rule that nginx $2 always becomes Envoy \1

(?i) keeps the case-insensitive match from the INFO line above.

yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: HTTPRouteFilter
metadata:
  name: files-capture-rewrite
  namespace: shop
spec:
  urlRewrite:
    path:
      type: ReplaceRegexMatch
      replaceRegexMatch:
        pattern: '(?i)^/files(?:/|$)(.*)$'
        substitution: '/\1'
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: regex-rewrite-files-example-test
  namespace: shop
spec:
  hostnames:
  - files.example.test
  parentRefs:
  - name: nginx
  rules:
  - matches:
    - path:
        type: RegularExpression
        value: (?i)/files(/|$)(.*)
    filters:
    - type: ExtensionRef
      extensionRef:
        group: gateway.envoyproxy.io
        kind: HTTPRouteFilter
        name: files-capture-rewrite
    backendRefs:
    - name: web
      port: 8080

ExtensionRef tells the HTTPRoute to run an implementation-specific filter. Here it points to the Envoy Gateway HTTPRouteFilter named files-capture-rewrite.

ReplaceRegexMatch rewrites the part of the path matched by the regex. Anchoring the pattern with ^ and $ makes this example operate on the whole request path.

Apply the prefix route from the previous section together with this filter and route. The HTTPRoutes already exist, so apply reports configured. The filter is new:

bash
kubectl apply -f shop-rewrite-fix.yaml
output
httproute.gateway.networking.k8s.io/prefix-rewrite-shop-example-test configured
httproutefilter.gateway.envoyproxy.io/files-capture-rewrite created
httproute.gateway.networking.k8s.io/regex-rewrite-files-example-test configured

Both routes should be accepted, and the references on each route should resolve. Select the conditions by type so the columns do not depend on list order:

bash
kubectl -n shop get httproute -o \
  custom-columns='NAME:.metadata.name,ACCEPTED:.status.parents[*].conditions[?(@.type=="Accepted")].status,RESOLVED:.status.parents[*].conditions[?(@.type=="ResolvedRefs")].status'
output
NAME                               ACCEPTED   RESOLVED
prefix-rewrite-shop-example-test   True       True
regex-rewrite-files-example-test   True       True

Read the two columns separately:

  • Accepted=True means Envoy accepted the HTTPRoute
  • ResolvedRefs=True means the references on the route resolved, including the web Service and, on the regex route, the HTTPRouteFilter

/shop/hostname now keeps the suffix. This is ReplacePrefixMatch, not the original rewrite-target: / behavior:

bash
curl -sS --resolve shop.example.test:19080:127.0.0.1 http://shop.example.test:19080/shop/hostname
output
/hostname

/shopping no longer matches, so Envoy returns an empty 404 instead of the / that ingress-nginx sent:

bash
curl -sS -D - -o /dev/null --resolve shop.example.test:19080:127.0.0.1 http://shop.example.test:19080/shopping
output
HTTP/1.1 404 Not Found
content-length: 0

/Shop/hostname is a separate miss. PathPrefix is case-sensitive, and this route no longer has the (?i) expression the converter printed. The response is again 404 with content-length: 0.

/files/hostname now matches the nginx result:

bash
curl -sS --resolve files.example.test:19080:127.0.0.1 http://files.example.test:19080/files/hostname
output
/hostname

/files with nothing after it leaves the capture empty, so the upstream path is /. That is the same body ingress-nginx returned for rewrite-target: /$2 on this URL:

bash
curl -sS --resolve files.example.test:19080:127.0.0.1 http://files.example.test:19080/files
output
/

/Files/hostname still works because both the match and the filter pattern use (?i):

bash
curl -sS --resolve files.example.test:19080:127.0.0.1 http://files.example.test:19080/Files/hostname
output
/hostname

Case and path boundaries

The converter kept ingress-nginx's case-insensitive locations by printing (?i). Remove (?i) from the files match, leaving /files(/|$)(.*), and the capital path misses. The body was empty:

bash
curl -sS -w '\n%{http_code}\n' \
  --resolve files.example.test:19080:127.0.0.1 \
  http://files.example.test:19080/Files/hostname
output
404

The blank line is the empty body, and 404 is the status. Put (?i) back on that match so /Files/hostname returns /hostname again.

Once you replace those ingress-nginx regex locations, these matching differences become important:

  • PathPrefix /shop does not match /shopping. The generated expression (?i)/shop.* does. ingress-nginx also matched /shopping in this example because rewrite-target caused the host's paths to be rendered as case-insensitive regex locations.
  • PathPrefix and a regular expression without (?i) are case-sensitive. /Shop/hostname then misses.
  • A body of / or /$2 with HTTP 200 means the route matched and the rewrite sent that path. The proxy did not miss the request.

The URL-normalization warning is separate. Gateway API does not copy nginx's RFC 3986 normalization. If the app depends on encoded dots or slashes being merged before the rewrite, check those URLs on both controllers. These examples use plain paths only.


Compare ingress-nginx and Envoy rewrite behavior

The edited routes solve two different problems:

  • Shop rows use ReplacePrefixMatch, which keeps the suffix. That is not a copy of rewrite-target: /.
  • Files rows use ReplaceRegexMatch. That is the copy of rewrite-target: /$2.
Request ingress-nginx Generated Envoy route Edited Envoy route
/shop/hostname / / /hostname
/Shop/hostname / / 404, empty body
/shopping / / 404, empty body
/shop / / /
/files/hostname /hostname /$2 /hostname
/Files/hostname /hostname /$2 /hostname
/files / /$2 /
/other/hostname 404 HTML 404, empty body 404, empty body

An unmatched path on ingress-nginx is the default backend page:

bash
curl -sS -D - --resolve files.example.test:18080:127.0.0.1 http://files.example.test:18080/other/hostname
output
HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 146

ingress-nginx returns its default HTML 404 page. The same URL on Envoy is an empty 404, because no route matched:

bash
curl -sS -D - -o /dev/null \
  --resolve files.example.test:19080:127.0.0.1 \
  http://files.example.test:19080/other/hostname
output
HTTP/1.1 404 Not Found
content-length: 0

Troubleshooting

Symptom Likely cause Fix
Backend receives /$2 and returns 200 ReplaceFullPath copied the capture token as a fixed path Use HTTPRouteFilter ReplaceRegexMatch and a \1 substitution
/shop/hostname hits the app at / rewrite-target: / and the generated ReplaceFullPath replace the whole path Keep that rewrite if / is what nginx sent. Use ReplacePrefixMatch only when the suffix must survive
/shopping works on ingress-nginx and 404s on Envoy PathPrefix /shop does not match /shopping. The generated (?i)/shop.* expression does. ingress-nginx matched it here because rewrite-target rendered that host's paths as case-insensitive regex locations Keep the regular expression if that URL must stay on this route
/Shop or /Files works on ingress-nginx and 404s on Envoy The new match is case-sensitive Keep the (?i) match the converter printed, on both the HTTPRoute and the filter pattern
Empty 404, where nginx returned an HTML page No HTTPRoute matched the host and path Compare the match, including case and /shopping versus /shop
HTTP 200, but the body is / or /$2 The route matched and the rewrite sent that path Read the body before you treat the status as success. ReplacePrefixMatch or ReplaceRegexMatch is the change if the app expected the suffix

Summary

rewrite-target: / replaces the entire upstream path. For that behavior, ReplaceFullPath: / is the direct Gateway API equivalent. Use ReplacePrefixMatch only when you intentionally want to preserve the suffix.

A target such as /$2 is different because it depends on a regex capture group. Core Gateway API cannot expand that capture. On Envoy Gateway, use an HTTPRouteFilter with ReplaceRegexMatch and a substitution that matches the capture groups in your new regex.

Compare the path the backend actually receives. A 200 response only proves that the route matched and the backend answered. It does not prove the rewrite is correct. Case sensitivity and path-boundary behavior can also change when you replace ingress-nginx regex locations with PathPrefix.


References


Frequently Asked Questions

Does ReplacePrefixMatch do the same thing as rewrite-target /?

No. They send /shop/hostname to different paths:

  • rewrite-target: / replaces the entire path, so the backend receives /
  • ReplacePrefixMatch replaces only the matched prefix with /, preserving the remaining path, so the backend receives /hostname

Why does the backend receive the path /$2?

ReplaceFullPath stores one fixed string. It does not expand $1 or $2. ingress2gateway 1.0.0 copied /$2 into that field, and Envoy forwarded that literal path.

Does use-regex rewrite the path?

No. The annotation and the rewrite are separate:

  • use-regex: "true" changes the match into a regular expression
  • The rewrite is still rewrite-target
  • Expanding a capture group on Envoy is an HTTPRouteFilter with ReplaceRegexMatch, not a core URLRewrite field

Why does /shopping return 404 after the prefix change?

Three different matchers are involved:

  • A normal Kubernetes Prefix /shop does not match /shopping
  • In this ingress-nginx example, rewrite-target makes the controller use a regular-expression location for the host, which is why /shopping matched before the migration
  • The Envoy PathPrefix route restores normal element-wise prefix behavior

Why does /Shop work on ingress-nginx and fail on the new route?

The capital path matched on ingress-nginx because that host's locations were case-insensitive regular expressions. It misses on the new route for a different reason:

  • The converter printed (?i) to keep that behavior
  • PathPrefix is case-sensitive
  • A regular expression without (?i) is case-sensitive too

The request then misses, and Envoy returns an empty 404.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)