Infrastructure engineering for East Africa's operators, platforms and regulators See the work →
ingress-nginx Is Retired: Alternatives and What Breaks
DevOps 8 min read

ingress-nginx Is Retired: Alternatives and What Breaks

ingress-nginx is retired. We served ingress2gateway output on Envoy Gateway: a 15 s default timeout, an open admin route, and which alternative to pick.

CE
CloudSpinx Engineering
Infrastructure Engineering, Virtualization, Networking, Automation
28 October 2026
ingress-nginxgateway apiingress2gatewayenvoy gateway

Our converted route answered a 20-second report with a 504 at 15.0 seconds, and none of the migration tool's warnings had mentioned a timeout on that route. That was a lab run: five ordinary Ingresses moved off ingress-nginx, which the Kubernetes project retired in March 2026. The tool built for the move, ingress2gateway, flagged every annotation it could not translate. What it never saw were the defaults nobody had written into an annotation, and those are what changed underneath us.

CloudSpinx builds and runs Kubernetes clusters for engineering teams in Kenya and across East Africa, and the ingress layer is part of what we build and hand over in our managed Kubernetes work, so we ran the move ourselves before advising anyone which alternative to pick. ingress2gateway 1.2.0 converted five Ingresses carrying 16 different ingress-nginx annotations, and Envoy Gateway 1.9.3, run in its standalone mode on Envoy 1.40.2, served the result to a test backend. We did not run ingress-nginx itself. What this post says about its behavior comes from its own documentation and config template at the final release.

Is ingress-nginx really end of life?

Yes. Maintenance stopped in March 2026, the last releases (controller v1.15.1, v1.14.5 and v1.13.9) shipped on 19 March, and the GitHub repository is archived read-only. The retirement notice says what that means in one line: "no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered."

Nothing breaks on the day, which is the problem. The controller keeps serving, so nobody raises a ticket. In most cases one command with cluster-admin rights tells you whether you are on it:

kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx

Microsoft and SUSE still patch their own builds, for a while, and Chainguard publishes a fork of the controller with security fixes backported. Microsoft's security patches for the ingress-nginx-based application routing add-on on AKS end in November 2026, and its migration guide points you to a Gateway API version of the same add-on or to Application Gateway for Containers. SUSE Rancher Prime LTS patches critical and important CVEs in RKE2 1.35's Ingress NGINX component through December 2027. F5's NGINX Ingress Controller is a separate project and is not part of this retirement.

Which ingress-nginx alternative to use

Gateway API for most clusters, and Traefik where k3s or RKE2 already runs it. There are three options, and they cost very different amounts of work.

Gateway API is where the Kubernetes project points. Its Ingress documentation says the Ingress API "has been frozen" and recommends Gateway instead. You choose an implementation and rewrite each Ingress as a Gateway plus HTTPRoutes, which ingress2gateway drafts for you, with extra output for Envoy Gateway, kgateway, agentgateway, Airlock and Google's GKE gateway. On AKS, Microsoft's replacement runs on an Istio control plane.

Another Ingress controller keeps your Ingress objects and swaps the thing reading them. On k3s, and on RKE2 clusters created from v1.36, that controller is already Traefik, installed by default. SUSE's supported route off ingress-nginx is Traefik with a compatibility layer that, in SUSE's words, "can interpret many existing Ingress NGINX annotations". We did not test that layer, so read "many" as a list to check against your own annotations.

Staying put has a date on it: November 2026 on AKS, December 2027 on RKE2 1.35 with a Prime LTS subscription, and none at all for the upstream controller you installed yourself, because the Kubernetes project no longer patches it. The controller is one more component on the upgrade calendar your cluster already owes, and an easier one to forget.

A toll plaza with separate lanes for trucks, buses and cars, the sorting an ingress controller does for every hostname and path

When the choice is ours, we would take Gateway API on Envoy Gateway. Its emitter translated 6 of our 16 annotations against 4 for the default output, and the auth and rate-limit annotations it dropped have somewhere to go by hand: Envoy Gateway's SecurityPolicy carries basic auth and external auth, and its BackendTrafficPolicy carries rate limits. On a small k3s or RKE2 cluster, Traefik is the shorter move because the distribution already ships it, and RKE2's guide runs it beside ingress-nginx before the switch.

What ingress2gateway dropped

Of the 16 annotations, the default output translated 4: the two CORS annotations, regex paths and the read timeout. The Envoy Gateway output also carried the body-size limit and the IP allowlist. The other 12, or 10, each came with a warning line. Nothing was dropped silently, and that is to the tool's credit.

A warning does not stop the YAML applying, though, and everything that applied served traffic.

Five ingress-nginx Ingresses after ingress2gateway, served by Envoy Gateway
Five ingress-nginx Ingresses after ingress2gateway, served by Envoy GatewayA grid of five Ingresses and how each behaved after conversion with ingress2gateway 1.2.0 and serving by Envoy Gateway 1.9.3, for the default output and for the Envoy Gateway output. The admin route with an IP allowlist and basic auth answered anyone with no password in the default output and kept only the allowlist in the Envoy Gateway output. The reports route lost its external login in both, the legacy route lost its rewrite, the plain web route got a 15 second timeout, and the api route got a 20 minute timeout.INGRESSDEFAULT OUTPUTENVOY GATEWAY OUTPUTwebno annotations504 at 15 sno timeout set, Envoy default504 at 15 sno timeout set, Envoy defaultapiread 120, body 20m, rps 1020 min timeoutno body cap, no rate limit20 min timeout20Mi cap kept, no rate limitlegacy-apirewrite-target /$2rewrite droppedbackend gets /legacy/ordersrewrite droppedbackend gets /legacy/ordersadminallowlist, basic authopen to anyone200, no password askedpassword gone403 outside the allowlistreportsauth-url loginlogin goneanonymous request gets 200login goneanonymous request gets 200
  • protection or routing lost
  • behavior changed
The tool warned about every annotation it dropped, and the output still applied and served. The admin page answered 200 with no password from the default output.

The two outputs differ only in the body limit and the IP allowlist, the two annotations only the Envoy Gateway emitter translated. Basic auth, the login redirect, the rate limit and the rewrite were left for a person.

The admin route is the one to lose sleep over. It had an IP allowlist and basic auth, and the default output served it to a request from outside the allowlist with no password asked. The /reports route lost the external login in front of it under both outputs, and the API's limit of 10 requests a second is simply gone: nothing in either output limits that route. Treat every warning line, whatever its wording, as a route that cannot cut over yet.

On AKS, pass your class. Given our five Ingresses on the webapprouting.kubernetes.azure.com class and default flags, the tool printed "No resources found" and exited 0.

The defaults nobody wrote down

ingress2gateway reads Ingresses and Services. It never reads the controller's ConfigMap, where cluster-wide overrides live, and it cannot translate a default that was never written anywhere. Three of those changed in our run, and the one timeout we did write changed meaning.

A 15-second limit on every route you left alone

Envoy Gateway writes no timeout onto a route whose HTTPRoute has none, so Envoy's own default applies, and Envoy documents it as 15 seconds that last until "the upstream response has been completely processed". ingress-nginx's proxy-read-timeout defaults to 60 seconds and, per its own docs, counts only the gap "between two successive read operations, not for the transmission of the whole response".

Envoy's 15 second default route timeout against a slow report and a streamed export
Envoy's 15 second default route timeout against a slow report and a streamed exportA 25 second time axis. A CSV export that streams one row a second delivers all 25 rows straight from the backend. Through the converted route, with no timeout set, Envoy stops it at 15 seconds after 15 rows while the client already holds an HTTP 200 status. A report that answers after 20 seconds gets a 504 at 15 seconds.Export, directbackend only200, 25 rowsExport, via Envoyno route timeout200, 15 rows, resetReport at 20 svia Envoy504 at 15.0 s0 s5 s10 s15 sEnvoy default20 s25 s
  • row delivered
  • row never sent
Same backend, same request. Through the converted route the export ended at 15 seconds with a 200 status and 15 of 25 rows, which a client checking only the status code reads as success.

ingress-nginx's documented default is a 60 second read timeout counted between reads, so neither request comes near it there. Envoy's 15 seconds runs until the whole response has arrived.

The report got its 504 at 15.0 seconds. The streamed export was worse. Through Envoy the client received an HTTP 200, then 15 of the 25 rows, then a reset, so anything that checks only the status code saw a successful download of a short file.

Setting timeouts.request: 60s on the route let the 20-second report through in our run, and a 70-second one then failed at 60 seconds, as it should. Give every route an explicit timeout, sized to its longest legitimate response, before the cutover rather than after the first complaint.

Longer timeouts where you set one

The tool turns ingress-nginx's connect, read and send timeouts into one request timeout of ten times the largest, and its warning calls that a best-effort translation without giving the factor. Our proxy-read-timeout: "120" became 20 minutes. A backend that sat silent for 130 seconds got its 200, where ingress-nginx's documented 120-second read timeout would have fired first, and a request to a hung worker can now wait 20 minutes.

Upload caps that disappeared

ingress-nginx caps a request body at 1m unless told otherwise. Where the output wrote no limit, Envoy Gateway enforced none in our tests: a 2 MiB upload went through on the route with no annotation, and with the default output so did 25 MiB on the route annotated 20m. The tool's note on that annotation reads "Most Gateway API implementations have reasonable body size and buffering defaults." If an upload form leaned on the old cap, it now leans on nothing. The Envoy Gateway output did carry 20m across, and there the 25 MiB request got its 413.

Headers your application may read

ingress-nginx's template sets X-Real-IP, X-Forwarded-Host, X-Forwarded-Port and X-Scheme on every proxied request. Behind Envoy our backend received none of them, only x-forwarded-for, x-forwarded-proto, x-request-id and Envoy's own x-envoy-external-address. Code that logs or rate-limits on X-Real-IP gets no header after the move.

Checking these, route by route, is the slow part of the move. If you would rather hand it over, send the Ingress export through the scoping form and we mark each route for you.

Moving to Gateway API without surprises

  1. Inventory what you use, and save the controller's ConfigMap, because its overrides are invisible to ingress2gateway:

    kubectl get ingress -A -o json \
      | jq -r '.items[].metadata.annotations // {} | keys[] | select(startswith("nginx.ingress.kubernetes.io/"))' \
      | sort | uniq -c | sort -rn
  2. Run ingress2gateway with your ingress class and the emitter for your implementation, and keep its warnings next to the YAML it wrote.

  3. Write the dropped auth, allowlist and rate-limit rules by hand, and give every route an explicit request timeout and, where an upload cap mattered, a body limit.

  4. Run both controllers side by side and test each hostname against the new one with curl --resolve before DNS moves.

  5. Move one hostname at a time, with the record's TTL lowered a full TTL ahead, since resolvers can keep the old address for as long as the TTL allows. Leave ingress-nginx running until the last route is across.

A cluster on AKS with a few Ingresses and no annotations can follow Microsoft's guide on its own, checking timeouts on the way, and does not need us. An RKE2 1.35 cluster on Prime LTS has until December 2027, and should spend part of it testing Traefik's compatibility layer against its own annotations.

A controller installed from the upstream chart is an unpatched proxy in front of every service today. Send us kubectl get ingress -A -o yaml, the controller's ConfigMap and where the cluster runs, through the managed Kubernetes scoping form or on WhatsApp at +254 713 403 044. You get back every route marked translates, needs a rule written or needs a decision from you, the implementation we would use for your cluster, and one figure for the move. Reading it costs nothing and commits you to nothing. If you go ahead, we handle the move with your team, one hostname at a time.

WhatsApp