Skip to main content

Ingress NGINX Is Officially Dead: The Gateway API Migration a Self-Hosted PaaS Can't Skip

9 min readDora NodaDora Noda
Share

On March 24, 2026, kubernetes/ingress-nginx — the controller sitting in front of more Kubernetes clusters than any other — went read-only. No more releases. No more bugfixes. No more CVE patches, ever, for the component that terminates TLS and routes every byte of external traffic into your cluster. If you run a self-hosted PaaS that auto-provisions an Ingress and a cert-manager certificate every time a tenant pushes code, that component is almost certainly ingress-nginx, and it is now a security liability with an expiration date already in the past.

This isn't a slow-motion deprecation you can plan around next quarter. It already happened. The question left is which of the two sanctioned exits you take, and what your routing layer should look like on the other side. Here's the decision, made concretely: a head-to-head of the two paths, the actual YAML each one produces, and where the object ownership boundary should sit once a git-push platform is generating these resources instead of a human hand-writing annotations.

Why One Maintainer's Burnout Became Your Incident

Ingress-nginx's retirement wasn't a strategic sunset — it was a maintenance model that ran out of road. For years the project was kept alive by one or two people doing security triage after their day jobs, on a controller whose defining feature — arbitrary NGINX configuration injection via annotations — kept surfacing as a fresh CVE class. The Kubernetes Steering and Security Response Committees said as much directly in their January 2026 statement: the flexibility that made ingress-nginx the default choice for a decade is the same flexibility that made it unmaintainable.

The official retirement announcement set the terms in November 2025 — best-effort support until March 2026, then nothing. That date came and went. The GitHub repo is now archived and read-only, kept around for reference only. Any CVE discovered in ingress-nginx from this point forward — and given its history, one will be — has no upstream fix. Ever. You'd be patching it yourself, forking a project you don't maintain, in the one place in your stack that terminates TLS and decides which tenant's traffic goes where.

For a self-hosted PaaS this isn't abstract. Auto-provisioning routing per tenant deploy is table stakes — it's the whole "push a git repo, get an HTTPS URL" promise. Most of the self-hosted platforms that make that promise (and no small number of internal platform teams) built it on Ingress objects like this:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tenant-acme-web
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
    - hosts: ["acme.tenant-apps.example.com"]
      secretName: acme-web-tls
  rules:
    - host: acme.tenant-apps.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: acme-web
                port: { number: 8080 }

Every one of those nginx.ingress.kubernetes.io/* annotations is a bet on a controller that will never receive another security fix. That's the whole problem in one YAML block: the config format a platform generates on every deploy is now frozen against a project with no future.

Two Viable Exits — Compared Head to Head

SIG Network didn't just announce the retirement and walk away. The same window that closed out ingress-nginx opened its replacement tooling: Ingress2Gateway 1.0 shipped March 20, 2026, four days before the EOL date, specifically so nobody would be migrating without a tested path. But "migrate to Gateway API" and "migrate this week" are different asks, and the ecosystem converged on two distinct answers rather than one.

Traefik drop-in (stopgap)Gateway API migration (destination)
What changesSwap the controller binary; existing Ingress objects and nginx.ingress.kubernetes.io/* annotations are read as-isRewrite IngressGateway + HTTPRoute; annotations become structured fields
Migration effortNear-zero — Traefik's NGINX Ingress provider parses ingress-nginx annotations natively, no YAML rewritePer-resource conversion, one annotation set at a time (30+ common ones auto-translated)
ToolingTraefik's built-in NGINX-annotation provideringress2gateway 1.0, with controller-level integration tests proving behavioral equivalence
Downtime riskLow — same object model, new controller reading itLow if done incrementally (Traefik and other Gateway-API-native controllers can run both models side by side during transition)
Where it leaves youOff the dead project, but still on the Ingress API and its annotation-as-config patternOn the API SIG Network is actively investing in, with a structured resource model instead of magic strings
When to pick itYou need to be off ingress-nginx now and can't schedule a resource-model rewrite this quarterYou're generating these resources programmatically (i.e., you're a platform, not a one-off cluster) and want to stop re-encoding vendor-specific annotations forever

The Traefik team's own pitch is explicit that this is a two-phase plan, not a rival destination: decommission ingress-nginx onto Traefik first without touching your resource model, then modernize to Gateway API on your own timeline instead of under a CVE deadline. That's a reasonable move for a single cluster with a backlog of hand-written Ingress YAML nobody wants to touch under pressure.

It's the wrong move for a platform. If your control plane is the thing generating these resources on every git push, you don't have a backlog of legacy YAML to protect — you have a template. Rewriting a template once is cheaper than running an annotation-translating shim indefinitely, and Gateway API is where cert-manager, the ecosystem, and SIG Network's own roadmap are pointed. For a PaaS control plane specifically, the second column is the one worth building against.

What the Migration Actually Produces

This is the part the two-phase advice above skips past: what do you actually generate once you've moved? Not a kind: Ingress translated 1:1 — Gateway API splits the single Ingress object into three roles, and understanding that split is the actual value of the migration, not a cosmetic rename.

  • GatewayClass — cluster-scoped, one per controller (analogous to IngressClass). A platform installs this once, not per tenant.
  • Gateway — the listener: which hostnames/ports it accepts traffic on, and where TLS terminates. A platform typically owns a small, fixed number of these — often one per entry point or per node pool, shared across many tenants.
  • HTTPRoute — the routing rule: which hostname/path goes to which backend Service. This is the object a platform generates per tenant app, replacing the per-tenant Ingress from the example above.

Converting the earlier Ingress with ingress2gateway produces roughly this — a Gateway (created once, shared) plus an HTTPRoute (created per tenant deploy):

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: tenant-apps-gateway
spec:
  gatewayClassName: traefik
  listeners:
    - name: acme-web-https
      protocol: HTTPS
      port: 443
      hostname: acme.tenant-apps.example.com
      tls:
        certificateRefs:
          - name: acme-web-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: tenant-acme-web
spec:
  parentRefs:
    - name: tenant-apps-gateway
  hostnames: ["acme.tenant-apps.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: acme-web
          port: 8080
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: acme-web-tls
spec:
  secretName: acme-web-tls
  dnsNames: ["acme.tenant-apps.example.com"]
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

Two things disappear from this that were load-bearing in the ingress-nginx version, and both are improvements. First, nginx.ingress.kubernetes.io/rewrite-target and /ssl-redirect become structured HTTPRoute match/filter fields instead of opaque strings the controller has to parse and (historically) sometimes mis-parse into a config-injection bug. Second, TLS termination now belongs to the shared Gateway, not to each tenant's route — cert-manager's Gateway API integration issues the certificate against the Gateway listener, and it can even solve the ACME HTTP-01 challenge by creating a temporary HTTPRoute on the fly and deleting it once validation completes. A platform's control plane never has to hand-manage that dance per tenant.

This is also where ingress2gateway earns its "sanctioned" label rather than being just a convenience script: each of its 30+ supported ingress-nginx annotation translations is backed by controller-level integration tests that exercise real Gateway API controllers in live clusters and assert the converted resource behaves identically to the original annotation — not just that the YAML parses.

What a Git-Push PaaS's Routing Layer Should Own

Once the object model changes, the ownership boundary a platform's control plane should enforce changes with it — and this is the design decision that outlasts the migration itself:

  1. One Gateway (or a small, fixed set) owned by the platform, not the tenant. Tenants shouldn't get their own Gateway; they get an HTTPRoute attached to a shared one. This is what makes routing a platform primitive instead of per-tenant snowflakes.
  2. One HTTPRoute generated per deploy, replacing the per-tenant Ingress your build pipeline used to template out. Custom domains, path rules, and header-based routing all become structured HTTPRoute fields — inputs a platform's API can validate and diff, not opaque annotation strings it has to string-match.
  3. TLS lives on the Gateway listener, managed by cert-manager, not re-issued per tenant object. A platform's control plane requests a hostname; cert-manager and the shared Gateway handle issuance and renewal underneath it.
  4. No controller-specific annotations in the platform's own schema. The entire reason ingress-nginx's annotation surface became a CVE generator is that "arbitrary controller config via string" has no validation boundary. A platform that owns structured Gateway/HTTPRoute fields instead of re-exporting nginx annotations to its users has closed off that whole class of bug by construction.

This is precisely the shape bex's own routing layer targets: push a git repo, and the platform reconciles a per-tenant HTTPRoute against a shared, cert-manager-backed Gateway — never a hand-authored Ingress with vendor annotations baked into the platform's own config surface.

The Deadline Already Passed — the Decision Hasn't

Ingress-nginx's EOL isn't a future risk to plan for; it's a fact you're already operating under. The Traefik stopgap is the right call if you're carrying years of hand-written Ingress YAML and need the CVE exposure gone this week. But if you're a platform generating these resources from a template on every deploy, that template only needs to be right once — and Gateway API's Gateway/HTTPRoute split, with ingress2gateway doing the mechanical translation and cert-manager handling issuance, is where SIG Network, the annotation ecosystem, and the next decade of Kubernetes networking are all pointed at once.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, routed through Gateway API instead of a retired Ingress controller. 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