Reverse proxy

ingress-nginx is retired: what replaces proxy-body-size, proxy-read-timeout and the other proxy annotations?

Annotation by annotation: what ingress2gateway emits, the Gateway API field, Envoy Gateway policy and Traefik's ingress-nginx provider, with the defaults that change.

· 13 min read · How we verify this

Key points

  • Gateway API core has a portable answer for only part of the list: timeouts.request for the timeouts, RequestRedirect for ssl-redirect, RequestHeaderModifier for x-forwarded-prefix and upstream-vhost. Body size and buffer annotations have no Gateway API field and land in implementation policy, such as Envoy Gateway's BackendTrafficPolicy requestBuffer.limit.
  • ingress2gateway does not copy timeout values. It takes the largest of proxy-connect-timeout, proxy-read-timeout and proxy-send-timeout and multiplies it by 10, so proxy-read-timeout: "300" became timeouts.request: 50m0s in testing on v1.2.0.
  • The routes most likely to break are the ones with no timeout annotation. They ran on ingress-nginx's 60s read timeout, nothing is emitted for them, and Envoy Gateway applies Envoy's 15s route default.
  • With the default standard emitter, proxy-body-size is dropped with a warning. With --emitter=envoy-gateway it becomes requestBuffer.limit, and client-body-buffer-size on its own becomes the same hard 413 limit, which it never was in nginx.
  • Traefik v3.7's kubernetesIngressNGINX provider reads the annotations in place, including auth-url, proxy-buffering and a subset of configuration-snippet directives, but its provider-level defaults are not ingress-nginx's: proxyConnectTimeout is 60 against ingress-nginx's 5.

Nothing replaces them one for one. Gateway API has portable fields for the timeouts (timeouts.request), for ssl-redirect (a RequestRedirect filter) and for x-forwarded-prefix and upstream-vhost (a RequestHeaderModifier filter). It has no field at all for proxy-body-size, proxy-buffer-size or proxy-buffering, so those move into whatever policy resource your chosen implementation defines, and every default underneath them changes. The other route is to keep the annotations and swap the controller: Traefik v3.7's kubernetesIngressNGINX provider reads most of them as they are, with its own defaults.

The table below takes the proxy-behaviour annotations one at a time across four targets: what ingress2gateway emits for them, the portable Gateway API field if there is one, the Envoy Gateway policy field, and the Traefik provider. ingress2gateway behaviour was tested on v1.2.0 (the README entries quoted below are unchanged since v1.0.0); Envoy Gateway fields are from v1.9.2, Gateway API from v1.6.3, Traefik from the v3.7 reference documentation.

The annotation table#

Annotation (ingress-nginx default)ingress2gateway emitsPortable Gateway API fieldEnvoy Gateway v1.9Traefik v3.7 kubernetesIngressNGINX
proxy-body-size (1m)standard: dropped, with a warning. envoy-gateway: requestBuffer.limitNoneBackendTrafficPolicy spec.requestBuffer.limitSupported. Provider default proxyBodySize 1048576 bytes
proxy-connect-timeout (5)Folded into timeouts.request: largest of the three x 10rules[].timeouts.request (Extended)BackendTrafficPolicy spec.timeout.tcp.connectTimeout, default 10sSupported. Provider default 60
proxy-read-timeout (60)Same ruletimeouts.request, timeouts.backendRequestspec.timeout.http.requestTimeoutSupported. Provider default 60
proxy-send-timeout (60)Same ruleSameSameSupported. Provider default 60
proxy-buffer-size (4k)"Unsupported annotation" warningNoneNo direct equivalentSupported. Provider default 8192 bytes
proxy-buffering (off)"Unsupported annotation" warningNoneNo direct equivalentSupported. Provider default false
use-forwarded-headers (false)Nothing: ConfigMap key, not an annotationNoneClientTrafficPolicy spec.clientIPDetection.xForwardedFor.numTrustedHopsTraefik entryPoint forwardedHeaders.trustedIPs
ssl-redirect (true with TLS)HTTP route with RequestRedirect, scheme: https, 308; false omits itRequestRedirect filter (Core)The same HTTPRouteSupported; cannot opt out per route if enabled globally
x-forwarded-prefixRequestHeaderModifier setting the header, only with rewrite-targetRequestHeaderModifier (Core)The same HTTPRouteSupported
upstream-vhostRequestHeaderModifier setting HostRequestHeaderModifier, or URLRewrite hostname (Extended)The same HTTPRouteSupported
auth-url"Unsupported annotation" warningExternalAuth filter, experimental channelSecurityPolicy spec.extAuth.httpSupported; "Forward auth behaves differently than NGINX"
configuration-snippet"Unsupported annotation" warningNoneNo equivalentA listed subset of directives

Defaults in the first column are the ingress-nginx ConfigMap values, which is what an Ingress without the annotation actually ran with. Timeouts there are unitless seconds. Note the connect timeout: ingress-nginx ships proxy-connect-timeout: 5, not the 60s that plain nginx's proxy_connect_timeout defaults to.

What "retired" means for a running cluster#

The Kubernetes announcement of November 2025 gave March 2026 as the end of best-effort maintenance, after which "there will be no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered". The last controller releases were v1.15.1, v1.14.5 and v1.13.9, all published on 19 March 2026, and the kubernetes/ingress-nginx repository is now archived on GitHub. The same announcement says existing deployments keep working and installation artefacts stay available.

What stops is the fix for the next CVE, which makes this a scheduled migration rather than an outage: the old controller can keep serving while the new routes are tested beside it.

The two answers that both circulate#

Search for this and you will find two claims. One says ingress2gateway converts your annotations. The other says Gateway API has no body size or buffering, so those annotations are lost. Both are true, and the switch between them is a single command line flag.

ingress2gateway reads Ingress objects and writes Gateway API objects through an emitter. The default is standard, which "will only output Gateway API". Because Gateway API has no body size field, the standard emitter cannot express proxy-body-size and reports it:

text
Failed to apply default.app.metadata.annotations."nginx.ingress.kubernetes.io/proxy-body-size"
from default/app: Most Gateway API implementations have reasonable body size and buffering defaults

Run the same input with --emitter=envoy-gateway and the annotation reappears as an Envoy Gateway resource. An Ingress carrying proxy-body-size: "50m" produced this alongside the HTTPRoute:

yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: app-app-example-com
spec:
  requestBuffer:
    limit: 50Mi
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: app-app-example-com

The other emitters are agentgateway, airlock-microgateway, gce and kgateway. Pick the one for the implementation you are moving to, because standard throws away everything only an implementation policy can hold. The README says the tool "is not intended to copy annotations from Ingress to Gateway API", which is the right way to read its output.

Timeouts: the x10 rule, and the routes that get 15 seconds#

The three ingress-nginx timeouts are nginx's proxy_connect_timeout, proxy_read_timeout and proxy_send_timeout. The read and send timeouts are gaps between two successive operations, not a limit on the whole request, a distinction timeout budgets across proxy hops covers in detail. Gateway API's timeouts.request is the opposite kind of timer. The specification says it "is intended to cover as close to the whole request-response transaction as possible", and when it is unset "request timeout behavior is implementation-specific".

ingress2gateway bridges the two with a fixed formula. Its common emitter takes the largest of the three annotation values on the Ingress and multiplies it by a constant, tcpTimeoutMultiplier = 10, which has been in the source since the v1.0.0 tag. Tested on v1.2.0:

Annotations on the IngressEmitted timeouts.request
proxy-read-timeout: "60"10m0s
proxy-connect-timeout: "5", proxy-read-timeout: "300", proxy-send-timeout: "60"50m0s
NoneNothing: no timeouts block at all

Every converted route also carries the warning "i2gw has made a best-effort translation to Gateway API timeouts.request. Please verify that this meets your needs." Take it literally. A 50 minute total deadline is a loose upper bound for a route that once allowed 300 seconds of silence between reads, but it is not the same control: a response that keeps trickling for an hour is now cut where nginx would have let it run. Set timeouts.request to the longest legitimate response time you expect on that route, and use timeouts.backendRequest for the per-attempt limit if your implementation supports retries. The specification requires backendRequest not to exceed request, since one request "may result in more than one call" to the backend.

The connect timeout has no portable field. Envoy Gateway puts it in BackendTrafficPolicy as spec.timeout.tcp.connectTimeout, documented in the API source as "Default: 10 seconds", which is double ingress-nginx's 5. The same spec.timeout.http block holds requestTimeout and a connectionIdleTimeout that defaults to one hour.

Body size and buffers: no portable field, and one surprise#

Gateway API v1.6.3 has no request body size or proxy buffering field on HTTPRoute. The only body size in the HTTPRoute types is inside the experimental external auth filter, where it limits how much body is sent to the authorization server. Every implementation therefore invents its own, and the semantics differ.

In Envoy Gateway v1.9.2, requestBuffer.limit is documented as "the maximum allowed size in bytes for each incoming request buffer. If exceeded, the request will be rejected with HTTP 413 Content Too Large." That matches what proxy-body-size did in ingress-nginx, which returned a 413 when "the size in a request exceeds the maximum allowed size of the client request body". The mechanism is different, though. Envoy Gateway's request buffering task says it "requires Envoy to fully receive the request before forwarding it upstream" and "does not work with streaming or upgrade-based traffic such as gRPC streaming and WebSocket", where requests "can hang indefinitely". A BackendTrafficPolicy that ingress2gateway attached to a route serving WebSockets is a bug you did not have before. Check every generated policy against the routes it targets, and see WebSockets through a reverse proxy for what those routes need instead.

The surprise is client-body-buffer-size. In nginx that is the in-memory buffer for a request body before it spills to a temporary file; it is not a limit. ingress2gateway's README lists it as converted, and the Envoy Gateway emitter's source says "Prefer body max size if present, otherwise fall back to body buffer size". Tested, an Ingress with client-body-buffer-size: "1m" and no proxy-body-size produced requestBuffer.limit: 1Mi. A route that accepted 1 MiB in memory and up to the default limit on disk now rejects anything over 1 MiB with a 413. When both annotations are present, proxy-body-size wins and the tool warns that the buffer size is ignored.

proxy-buffer-size and proxy-buffering have no target in ingress2gateway at all: both produced "Unsupported annotation" warnings with the standard and Envoy Gateway emitters. They control nginx's response side. proxy-buffer-size sizes the buffer for the first part of the upstream response, which is where the upstream sent too big header error in header size limits comes from. Envoy does not buffer responses the way nginx does, as proxy buffering explains, so there is no field to copy the value into. The nearest Envoy Gateway setting, connection.bufferLimit (32768 bytes by default on both the client and the backend side), is a soft limit on connection buffers, not the same control, and it should not be raised just because an old annotation said 16k.

Redirects, headers and the prefix#

These are the annotations Gateway API core handles well.

  • ssl-redirect. ingress-nginx redirects to HTTPS with a 308 when the Ingress has TLS. ingress2gateway reproduces it as a separate HTTPRoute on the port 80 listener with a RequestRedirect filter, scheme: https, statusCode: 308. With ssl-redirect: "false" the redirect route is not generated. RequestRedirect is Core support, so every conformant implementation honours it.
  • upstream-vhost. Converted to a RequestHeaderModifier filter that sets Host. Gateway API also has a purpose-built field, URLRewrite hostname, described as "the value to be used to replace the Host header value during forwarding", but that filter is Extended support where the header modifier is Core.
  • x-forwarded-prefix. The README says it is converted "when used alongside rewrite-target", and the condition is strict. An Ingress with x-forwarded-prefix: "/app" and no rewrite-target produced an HTTPRoute with no header and no warning. The same annotation next to rewrite-target produced a URLRewrite filter plus a RequestHeaderModifier setting X-Forwarded-Prefix: /app. If the application relies on the header, check for it in the output rather than in the log. Why the header matters, and why the application has to be told to read it, is in running an app under a subpath.

Real client IP: a ConfigMap key no converter reads#

use-forwarded-headers is not an Ingress annotation. It is an ingress-nginx ConfigMap key, default false, and when enabled nginx passes incoming X-Forwarded-* headers through instead of building its own. ingress2gateway converts Ingress objects and their annotations; its ingress-nginx provider has no code that reads the controller ConfigMap, and the same is true of compute-full-forwarded-for, the global proxy-body-size and the global timeouts. Cluster-wide settings have to be carried across by hand.

Envoy Gateway puts client IP trust in ClientTrafficPolicy, attached to the Gateway: with spec.clientIPDetection.xForwardedFor.numTrustedHops set, "the client IP is taken from the Nth address from the right end of the XFF header". Traefik puts it on the entryPoint with forwardedHeaders.trustedIPs, and its documentation recommends forwardedHeaders.insecure "only for tests purposes, not in production". Hop counting and CIDR trust fail in different ways when the topology changes, which configuring trusted proxies compares.

auth-url and configuration-snippet#

auth-url produced an "Unsupported annotation" warning from ingress2gateway. Gateway API v1.6.3 does define an ExternalAuth HTTPRoute filter, but it is marked <gateway:experimental>, so it exists only in the experimental channel CRDs. In Envoy Gateway the supported route is a SecurityPolicy with extAuth.http.backendRefs and headersToBackend; its documentation says a request "deemed unauthorized" is "denied with a 403 (Forbidden) response". Traefik's provider supports auth-url, auth-signin and auth-response-headers, with the caveats "Only URL and response headers copy supported" and "Forward auth behaves differently than NGINX". The difference that matters, what each proxy does when the auth service answers 302, is in nginx auth_request vs Traefik ForwardAuth vs Caddy forward_auth.

configuration-snippet was already disabled on a default install: allow-snippet-annotations defaults to false in the ConfigMap, and the annotation documentation warns that snippets "can be dangerous in multi-tenant clusters", citing CVE-2021-25742. ingress2gateway reports it as unsupported, and no Gateway API field can hold raw proxy configuration. Each line has to become an explicit filter or policy. Traefik is the exception: its provider accepts add_header, proxy_method, more_set_headers, proxy_set_header, more_set_input_headers, set, if and return code [text], with "minimal variable interpolation". Anything outside that list needs rewriting either way.

Which path to take#

Move to Gateway API with ingress2gateway when you want off the Ingress API as well as off the controller, and you have time to read every warning. Choose the emitter for your target implementation, then work through the output in this order: routes with no timeout annotation, every generated BackendTrafficPolicy against streaming routes, every client-body-buffer-size, x-forwarded-prefix without rewrite-target, and the ConfigMap keys nothing translated.

Swap to Traefik's kubernetesIngressNGINX provider when the immediate goal is to stop running unpatched software with the fewest manifest changes. It watches IngressClass nginx and controller k8s.io/ingress-nginx by default, and its provider options mirror the ConfigMap keys. Set them deliberately, because several defaults differ from what ingress-nginx shipped:

Settingingress-nginx ConfigMap defaultTraefik provider default
Connect timeout5proxyConnectTimeout 60
Read and send timeouts6060
Request body limit1mproxyBodySize 1048576 bytes
Response buffer size4kproxyBufferSize 8192 bytes
Client body buffer8kclientBodyBufferSize 16384 bytes
Request bufferingonproxyrequestbuffering false
Retrieserror timeout, 3 trieserror timeout, 3 tries

Retries are not identical either: the documentation notes that "Unlike NGINX, Traefik does not guarantee that retries are sent to a different server". The wider trade between Envoy-based gateways and Traefik is in nginx vs HAProxy vs Envoy vs Caddy vs Traefik, and Traefik's own object model in Traefik routers, services and middlewares.

Where this stops applying#

Every field and default here is version specific. ingress2gateway behaviour was tested on v1.2.0 with --input-file and the standard and envoy-gateway emitters only; the x10 multiplier is a constant in the source and could change in a later release. Envoy Gateway's API is v1alpha1, and its main branch already adds a mode to requestBuffer that is not in v1.9.2. Other Gateway API implementations have their own policy resources and their own defaults, and the Envoy Gateway column does not apply to them: read the implementation's API reference for the exact field and default before relying on any value. Traefik behaviour is from its v3.7 documentation and was not tested here.

Frequently asked questions#

What replaces nginx.ingress.kubernetes.io/proxy-body-size in Gateway API?#

Nothing portable. Gateway API has no request body size field, so the limit moves into an implementation policy. In Envoy Gateway it is BackendTrafficPolicy spec.requestBuffer.limit, which returns a 413 above the limit and fully buffers the request, so keep it off WebSocket and gRPC streaming routes. ingress2gateway emits it only with --emitter=envoy-gateway (or another implementation emitter); the default standard emitter drops it with a warning.

What replaces proxy-read-timeout in Gateway API?#

timeouts.request on the HTTPRoute rule, with timeouts.backendRequest for a single attempt. They are total deadlines, not gaps between reads. ingress2gateway sets timeouts.request to the largest of the three timeout annotations multiplied by 10, so proxy-read-timeout: "60" becomes 10m0s.

Why do my requests time out at 15 seconds after moving to Envoy Gateway?#

Because the route has no timeouts.request and Envoy's default request timeout of 15 seconds applies. On ingress-nginx the same route had a 60 second read timeout from the ConfigMap. ingress2gateway only emits a timeout for Ingresses that carried a timeout annotation, so set one explicitly on any route that can take longer than 15 seconds.

Does ingress2gateway convert the ingress-nginx ConfigMap?#

No. It converts Ingress objects and their annotations. Global settings such as use-forwarded-headers, compute-full-forwarded-for, the cluster-wide proxy-body-size and the default timeouts have to be recreated by hand, for example in an Envoy Gateway ClientTrafficPolicy or BackendTrafficPolicy attached to the Gateway.

Is there a Gateway API equivalent of auth-url?#

Only in the experimental channel. Gateway API v1.6.3 defines an ExternalAuth HTTPRoute filter marked experimental. In practice you use the implementation's resource: a SecurityPolicy with extAuth in Envoy Gateway, or the auth-url annotation itself under Traefik's kubernetesIngressNGINX provider.

Primary sources#

Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.

  1. Kubernetes blog, Ingress NGINX Retirement
  2. kubernetes/ingress-nginx repository (archived; releases)
  3. ingress-nginx, Annotations
  4. ingress-nginx, ConfigMap
  5. ingress2gateway, ingress-nginx provider README and source
  6. Gateway API, HTTPRoute types (timeouts, filters, ExternalAuth)
  7. Envoy Gateway, HTTP timeouts
  8. Envoy Gateway, request buffering
  9. Envoy Gateway, API types v1.9.2 (Timeout, BackendConnection, ClientConnection)
  10. Traefik, Kubernetes Ingress NGINX provider and annotation reference

Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.

More in reverse proxy configuration#