Skip to main content

Ingress-NGINX Is Already Retired: An Ingress2Gateway Migration Walkthrough for Self-Hosted Clusters

8 min readDora NodaDora Noda
Share
On this page

Ingress-NGINX didn't announce a retirement date somewhere off in the future. It announced one, and then that date passed. The project went from best-effort maintenance to fully retired — repository archived, read-only, zero further releases — in March 2026. It is now the end of July 2026. If your cluster is still routing production traffic through Ingress-NGINX, you have been running an unpatched ingress controller for four months, on a component that terminates every external request before it reaches your workloads.

That's a different risk profile than "we should get around to this." Here's what the retirement timeline actually looked like, and a concrete walkthrough of migrating a self-hosted, Cluster API-managed Hetzner fleet off Ingress-NGINX with ingress2gateway — including the two things it won't convert for you.

The timeline that already happened

Kubernetes SIG Network and the Security Response Committee announced the retirement on November 11, 2025. The stated reason wasn't a roadmap decision — it was that the project "has been maintained solely by one or two people working in their free time," and the accumulated technical debt made it "no longer reasonable or even possible to continue maintaining the tool" safely. Ingress-NGINX sits in front of roughly half of all Kubernetes clusters, which made this one of the larger single-component deprecations the ecosystem has absorbed.

A joint statement on January 29, 2026 reiterated the deadline with about two months left on the clock, warning explicitly that "choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack." Then, as scheduled, March 2026 arrived: best-effort maintenance stopped, the repository was archived, and the committees confirmed there would be "no more releases for bug fixes, security patches, or any updates of any kind."

Existing deployments didn't stop working — that's the trap. Nothing breaks on retirement day, so it's easy to mistake "still running" for "still fine." What actually changed is that every CVE found in Ingress-NGINX from March 2026 onward has no patch coming, ever, from upstream. The clock that matters isn't "when do we lose functionality," it's "how long has our edge proxy been unpatched," and for anyone who hasn't moved yet, that number is already in the months.

Walking a real Ingress manifest through ingress2gateway

The Kubernetes project's own recommended path is ingress2gateway, which reached its 1.0 release on March 20, 2026 with support for over 30 common Ingress-NGINX annotations — a large jump from the handful the early versions handled. It's a conversion assistant, not a one-click migration: it translates what it can, and flags what it can't.

Take a representative Ingress object off a self-hosted, Cluster API-provisioned Hetzner fleet (CAPH) — the kind of manifest that accumulates a few years of real annotations rather than a clean tutorial example:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: storefront
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/proxy-body-size: 20m
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Frame-Options: DENY";
spec:
  tls:
    - hosts: [storefront.example.com]
      secretName: storefront-tls
  rules:
    - host: storefront.example.com
      http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service: { name: storefront-api, port: { number: 8080 } }

Running ingress2gateway print against this (targeting Envoy Gateway as the Gateway API implementation) produces a Gateway and HTTPRoute pair. Envoy Gateway is worth naming specifically rather than "a Gateway API implementation" in the abstract, because the choice isn't neutral on a self-hosted, non-cloud-managed fleet: it's CNCF-conformant against the full Gateway API test suite, ships as a self-contained control plane with no dependency on a cloud provider's managed Gateway product, and its EnvoyExtensionPolicy/BackendTrafficPolicy CRDs are what actually absorb the annotations that don't have a standard Gateway API field. Cilium and Istio's gateway mode are the other common self-hosted picks — both conformant, both viable — but they pull in a full CNI or service-mesh commitment most Ingress-NGINX migrations aren't otherwise signing up for. Envoy Gateway installs as an ingress-layer swap and nothing more, which keeps the blast radius of the migration itself small.

Here's what happens to each annotation from the manifest above:

Ingress-NGINX annotationingress2gateway 1.0What lands in the output
rewrite-target: /$2✅ Auto-translatedAn HTTPRoute URLRewrite filter with the equivalent path-replace behavior
proxy-body-size: 20m✅ Auto-translatedEnvoy Gateway BackendTrafficPolicy setting the equivalent request size limit
enable-cors + cors-allow-origin✅ Auto-translatedA native Gateway API CORS filter on the HTTPRoute — no annotation needed at all going forward
cert-manager.io/cluster-issuer⚠️ Not carried overTLS terminates on the Gateway, not the HTTPRoute — this annotation has to be added by hand
configuration-snippet❌ Untranslatable, flaggedRaw nginx config directives have no Gateway API equivalent; the tool surfaces it as unsupported rather than dropping it silently

Three of five convert cleanly. Two need a human, and the tool is explicit about the difference rather than pretending everything transferred — that's the actual state of "concrete walkthrough" once you run it against annotations that look like your production cluster instead of a demo.

The two gotchas, and how to close them

cert-manager moves to the Gateway, not the HTTPRoute. Ingress-NGINX's model puts the TLS issuer annotation on the same object as the routing rules. Gateway API splits those concerns: TLS termination is a property of the Gateway's listeners[].tls block, and cert-manager's gateway-shim controller watches for its issuer annotation there, not on HTTPRoute. ingress2gateway generates the Gateway object but doesn't know which ClusterIssuer you want, so it leaves the annotation off. The fix is one field, but it's a field you have to add — not one you get for free:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod   # add this by hand
spec:
  listeners:
    - name: https
      protocol: HTTPS
      tls:
        certificateRefs:
          - name: storefront-tls

configuration-snippet has no home. This annotation exists specifically because Ingress-NGINX lets you inject raw nginx config when the structured annotations run out — which means whatever's in it is, by definition, the one behavior with no portable equivalent. Envoy Gateway's answer is an EnvoyExtensionPolicy (or, for simpler header injection like the example above, a ResponseHeaderModifier filter on the HTTPRoute — Gateway API's built-in mechanism for exactly this case). Either way, this is manual re-implementation, not conversion; budget real time for every snippet your cluster has accumulated, because each one has to be read, understood, and rebuilt against a different config surface.

The cutover sequence

None of this requires a maintenance window if it's sequenced right, because Gateway API and Ingress-NGINX can run side by side on the same cluster pointed at the same backends:

  1. Install the Gateway API CRDs (Standard channel) and deploy Envoy Gateway alongside the existing Ingress-NGINX controller — not replacing it yet.
  2. Let the Hetzner CCM provision a second Load Balancer. The hcloud-cloud-controller-manager already running on a CAPH-managed fleet auto-provisions a Hetzner Load Balancer for any Service of type: LoadBalancer — which is exactly how the existing Ingress-NGINX Service got its IP in the first place. Envoy Gateway's Service gets its own, independent Hetzner LB the same way, with no new cloud integration to wire up.
  3. Apply the generated Gateway/HTTPRoute objects, patched with the cert-manager annotation and the rebuilt snippet policy, and let cert-manager issue a fresh certificate against the new listener.
  4. Validate against the new LB IP directlycurl --resolve storefront.example.com:443:<new-lb-ip> — confirming rewrites, CORS, body-size limits, and headers all match production behavior before any real traffic sees it.
  5. Cut DNS over once validation passes, and watch both controllers' logs during propagation.
  6. Delete the legacy Ingress objects and decommission the Ingress-NGINX deployment and its Load Balancer once traffic has fully drained to the new path.

Every step up through DNS cutover is reversible — the old Ingress and its LB keep serving traffic on the old IP the entire time you're validating the new one. Rolling back means pointing DNS back at the old IP, nothing more; there's no destructive step until you explicitly delete the legacy objects in step six. That's what makes this a scheduled task with a bounded time cost, not a fragile parachute you're going to want to keep putting off.

A fleet with more than one Ingress object should triage rather than convert everything at once. Run ingress2gateway print across the whole namespace first, then sort by output: the routes that come back fully auto-translated (the rewrite-target/proxy-body-size/CORS pattern above) can move in a batch, since there's no manual work per route beyond re-adding the cert-manager annotation once per Gateway. Routes that flag configuration-snippet or another unsupported annotation should get pulled into a separate list and migrated one at a time, because each is its own review — not because the process is different, but because that's where the actual engineering time is.

Treat the retirement date as already past, because it is

The framing worth dropping is "eventually we should migrate off Ingress-NGINX." It's already retired. Every month spent on it after March 2026 is a month running an ingress controller with a known, public track record of security issues and an archived repository that will never ship a fix for the next one. ingress2gateway converts the mechanical majority of a real manifest correctly; the TLS annotation and any custom snippets are the fixed, boundable cost of the rest — not a reason to wait for a "someday" that keeps every request on an unmaintained code path in the meantime.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on Cluster API-managed machines you own, with the ingress layer treated as infrastructure the platform manages rather than a component your team has to individually keep current. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex