Ingress NGINX retirement: revisit your Kubernetes ingress before you migrate

A platform team plans its Kubernetes 1.37 upgrade for the next sprint. The cluster checklist is green, except one line nobody owns: the ingress-nginx Helm chart, pinned at 4.15.1, in front of forty services. A security reviewer asks a simple question: “Who patches this proxy now?” Since March 2026 the honest answer is “nobody”. The controller still routes traffic, so nothing feels urgent. That is exactly why it needs a plan.

This post covers what the retirement means, where Gateway API stands, how ingress-nginx annotations map (and where they do not), how to choose a replacement, and a migration plan. Facts come from the kubernetes.io blog and docs, the archived kubernetes/ingress-nginx repository, Gateway API v1.6.2 release files and conformance reports, ingress2gateway v1.2.0 docs and source, and the projects’ own docs, checked on 6 October 2026. Drill results are from my own run.

The short version

  • Community Ingress NGINX (kubernetes/ingress-nginx) is retired. Its last releases came out in March 2026 and the repository was archived on 24 March 2026. Existing installs keep working; there are no more fixes, including security fixes.
  • The Ingress API is not going away. It is frozen, and Kubernetes recommends Gateway API instead.
  • F5’s NGINX Ingress Controller and NGINX Gateway Fabric are different, maintained projects. Do not confuse them with the retired one.
  • Gateway API v1.6.2 is the current release. Gateway, HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute, ListenerSet, BackendTLSPolicy and ReferenceGrant are in the Standard channel. Retries, session persistence and external auth are still experimental.
  • Many annotations have no Standard equivalent: rate limits, auth, snippets, cookie affinity, body size, IP allow lists. You will need your chosen implementation’s own policies for these.
  • ingress2gateway 1.x is a good first draft, not a final answer. In my drill it reported clearly what it skipped, but the output still needed human review.

Where things stand on 6 October 2026

Date

Event

24 March 2025

Critical ingress-nginx CVEs, including CVE-2025-1974, patched in v1.12.1 and v1.11.5

11 November 2025

SIG Network and the Security Response Committee announce the retirement for March 2026

29 January 2026

Joint statement from the Steering and Security Response Committees

27 February 2026

Gateway API v1.5.0 released

19 March 2026

Last tags: controller-v1.15.1, v1.14.5 and v1.13.9

20 March 2026

ingress2gateway 1.0 announced

24 March 2026

kubernetes/ingress-nginx archived, now read-only

30 June 2026

Gateway API v1.6.0 released (v1.6.2 on 3 September 2026)

7 July 2026

ingress2gateway v1.2.0 tagged

26 August 2026

Kubernetes v1.37 released

The final README lists v1.15.1 as tested on Kubernetes 1.31 to 1.35. Kubernetes is now at 1.37, so every upgrade from here moves you further from what was tested.

What retirement means, and what it does not

The November 2025 announcement is clear: after March 2026, “no further releases, no bugfixes, and no updates to resolve any security vulnerabilities”. Existing deployments are not broken, and images and Helm charts stay available. The January 2026 statement adds a line worth printing for your planning meeting: “None of the available alternatives are direct drop-in replacements.” It also cites internal Datadog research that about half of cloud native environments rely on this controller.

The final mailing list note from the maintainers says the Kubernetes Security Response Committee will still issue CVEs for ingress-nginx code, but will not ship patch releases for issues reported after end of life. So you may get a CVE number with no fix.

The announcement gives two reasons: too few maintainers for years, and flexibility that became risk, such as snippet annotations that inject raw NGINX configuration. The replacement project, InGate, never matured and is also being retired.

Three things did not change:

  • The Ingress API itself. The Kubernetes docs say it is generally available, frozen, and there are no plans to remove it. In my drill, a v1.37 API server accepted Ingress objects as usual.
  • Other Ingress controllers. Many are still maintained, so staying on Ingress with a different controller is a valid choice.
  • F5’s products. The F5 NGINX Ingress Controller (nginx/kubernetes-ingress, annotations under nginx.org/) had release v5.6.3 in September 2026. A February 2026 kubernetes.io post spells it out: both use NGINX as the data plane “but are otherwise unrelated”.

First, find out what you actually run

The announcement suggests this check, with cluster admin rights:

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

# Which ingress-nginx annotations do your Ingresses use, and how often?
kubectl get ingress -A -o json \
  | jq -r '.items[].metadata.annotations // {} | keys[]' \
  | grep '^nginx.ingress.kubernetes.io/' | sort | uniq -c | sort -rn

I tested the jq pipeline on my sample manifests. Annotations are only part of the picture. Also export the controller ConfigMap and the controller arguments. Global settings such as forwarded header handling, the default certificate, or --enable-ssl-passthrough change behaviour for every Ingress. ingress2gateway reads Ingress objects and provider CRDs, not your controller flags. The final annotations page mentions about 130 distinct nginx.ingress.kubernetes.io/ keys, so your own inventory, not the full list, decides how hard the migration will be.

Gateway API in one page

Ingress puts the entry point and the routing in one object, and the controller decides the rest through annotations. Gateway API splits these into roles:

  • GatewayClass: which implementation runs the proxy. Usually installed by the platform team or the provider.
  • Gateway: listeners (ports, protocols, hostnames, certificates) and which namespaces may attach routes. By default allowedRoutes allows only the Gateway’s own namespace.
  • HTTPRoute and friends: matches, filters and backends, owned by the application team.

Every feature also has a support level. The conformance guide describes Core as portable with a roadmap for all implementations, Extended as portable but not universally supported (same behaviour where supported), and Implementation-specific as not portable.

What is Standard in v1.6.2

From the CRDs in the v1.6.2 standard-install.yaml:

Resource

Standard channel

Served version

GatewayClass, Gateway, HTTPRoute

Yes

v1 (v1beta1 still served)

GRPCRoute

Yes

v1

TLSRoute, ListenerSet

Yes, since v1.5

v1

TCPRoute, UDPRoute

Yes, since v1.6

v1 (v1alpha2 deprecated)

BackendTLSPolicy

Yes

v1

ReferenceGrant

Yes

v1 and v1beta1

XBackend, XBackendTrafficPolicy, XMesh

No, experimental only

v1alpha1, new gateway.networking.x-k8s.io group

Inside HTTPRoute, the Standard channel has path, header, query and method matches; header modifiers, redirects, URL rewrites, mirroring and the CORS filter; weighted backends; and timeouts. The retry and sessionPersistence fields and the ExternalAuth filter exist only in the experimental CRDs.

Two practical points from the release notes. First, since v1.5 a ValidatingAdmissionPolicy, safe-upgrades, blocks installing experimental CRDs over standard ones and blocks downgrades. Second, CRDs are cluster wide, and the CRD guide says a cluster admin or provider should own them. Envoy Gateway’s compatibility matrix, for example, lists v1.9 as built with Gateway API v1.6.1. Decide who owns the CRD version before two teams install two charts.

How ingress-nginx annotations map

The third column shows what ingress2gateway v1.2.0 does with the default standard emitter, from its provider README and my run.

ingress-nginx annotation

Gateway API equivalent

ingress2gateway v1.2.0

ssl-redirect (308 by default)

RequestRedirect filter on the HTTP listener

Converted, 308

rewrite-target with a fixed path

URLRewrite filter

Converted

rewrite-target with $1, $2

None in Standard

Not converted, warning

use-regex

RegularExpression path match (implementation specific)

Converted to case-insensitive regex

canary, canary-weight

Weighted backendRefs

Converted

canary-by-header, canary-by-header-value

Header match

Converted

canary-by-cookie

None

Warning only

proxy-connect/send/read-timeout

timeouts.request, timeouts.backendRequest (different meaning)

Best effort, see the drill

enable-cors, cors-*

CORS filter (Standard since v1.5)

Converted

backend-protocol: GRPC or HTTPS

GRPCRoute, BackendTLSPolicy

Converted

ssl-passthrough

TLSRoute in Passthrough mode

Converted

proxy-body-size

None

Dropped with a warning; vendor policy with some emitters

whitelist-source-range

None

Dropped with a warning; vendor policy with some emitters

affinity: cookie

sessionPersistence (experimental)

Not supported

limit-rps, limit-rpm

None in either channel

Not supported

auth-url

ExternalAuth filter (experimental)

Not supported

configuration-snippet

None, by design

Not supported

Two behaviours deserve attention because they are invisible in a single YAML file. The ingress-nginx docs say that if use-regex or rewrite-target is set on any Ingress for a host, case-insensitive regex matching applies to all paths of that host, whatever Ingress they live in. And nginx’s proxy_read_timeout applies “between two successive read operations”, while Gateway API’s timeouts.request covers close to the whole request and response. A kubernetes.io post from February 2026, “Before You Migrate”, walks through these quirks with examples. Read it before your first conversion.

Choosing where to go

There are three sensible paths, and one risky one:

  1. Gateway API with a conformant implementation. This is what Kubernetes recommends, and it gives you the role split.
  2. Another Ingress controller. Fewer YAML changes now, but you stay on a frozen API. Traefik documents an Ingress NGINX provider that reads many ingress-nginx annotations, with listed differences. F5 publishes a migration guide to its NGINX Ingress Controller. Both are the vendors’ own descriptions.
  3. Your cloud’s load balancer controller, if you are on one provider and accept its limits.
  4. Stay on the retired controller. Only as a short, written exception with a deadline, extra controls in front, and nobody allowed to create Ingress objects without review.

For Gateway API, here are the main open source options. Versions are the latest GitHub releases on 6 October 2026. “Report” means a conformance report submitted for Gateway API v1.6.0 in the gateway-api repository.

Project

Data plane

Latest release

v1.6.0 report, HTTP core

Worth knowing

Envoy Gateway

Envoy

v1.9.2

v1.9.0, passed

Own policies for rate limits, auth, IP rules; v1.9 end of life 14 Feb 2027 per its matrix

Istio

Envoy

1.31.1

1.31, passed

Says it intends Gateway API to become its default traffic API

Cilium

Envoy, in the CNI

v1.20.2

v1.20.0, passed

Needs kubeProxyReplacement=true; it is a CNI decision too

NGINX Gateway Fabric (F5)

NGINX

v2.7.2

v2.7.0, passed

Creates an NGINX data plane Deployment per Gateway

Traefik Proxy

Traefik

v3.7.14

v3.7.10, partial (one test skipped)

Also has the Ingress NGINX compatibility provider

Kong Operator

Kong Gateway

v2.3.2 (also a v2.4.0-rapid.2.0 tag)

v2.3.2, passed

Kong says new Kubernetes work lands here, not in KIC

HAProxy Unified Gateway

HAProxy

v1.0.7

No v1.6.0 report

Its docs list compliance with Gateway API 1.5.0 experimental, 1.4.0, 1.3.0

Core results look the same, so compare extended features you actually use. From the same reports (HTTP profile):

Extended feature claimed

Envoy Gateway 1.9.0

Istio 1.31

Cilium 1.20.0

NGF 2.7.0

Traefik 3.7.10

Path rewrite

Yes

Yes

Yes

Yes

Yes

Request timeout

Yes

Yes

Yes

No

No

CORS filter

Yes

Yes

Yes

Yes

No

Request mirror

Yes

Yes

Yes

Yes

No

ListenerSet

Yes

Yes

Yes

Yes

No

“No” means not claimed in that report, not impossible. Vendor CRDs may cover it. Kong’s report depends on its router mode, so I left it out of this table.

Before and after

A small example: one Ingress plus a canary Ingress, and a hand-written Gateway API version that separates platform and application ownership. I applied the “after” manifests to a v1.37 API server with the v1.6.2 Standard CRDs. No controller was running, so this only proves they are valid.

# Before: ingress-nginx
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: orders
  namespace: orders
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
  ingressClassName: nginx
  tls:
  - hosts: [orders.example.in]
    secretName: orders-tls
  rules:
  - host: orders.example.in
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service: {name: orders-api, port: {number: 8080}}
# plus an "orders-canary" Ingress for orders-api-v2 with
# canary: "true" and canary-weight: "10"
---
# After: Gateway owned by the platform team (namespace infra)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public
  namespace: infra
spec:
  gatewayClassName: my-gateway-class   # from your implementation
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces: {from: Same}         # only the redirect route below
  - name: orders-https
    protocol: HTTPS
    port: 443
    hostname: orders.example.in
    tls:
      mode: Terminate
      certificateRefs:
      - name: orders-tls               # Secret now lives in infra
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels: {gateway-access: public}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: https-redirect
  namespace: infra
spec:
  parentRefs:
  - {name: public, sectionName: http}
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect: {scheme: https, statusCode: 308}
---
# After: route owned by the application team (namespace orders)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: orders
  namespace: orders
spec:
  parentRefs:
  - {name: public, namespace: infra, sectionName: orders-https}
  hostnames: [orders.example.in]
  rules:
  - matches:
    - path: {type: PathPrefix, value: /api}
    timeouts:
      request: 60s                     # a decision, not a copy of proxy-read-timeout
    backendRefs:
    - {name: orders-api, port: 8080, weight: 90}
    - {name: orders-api-v2, port: 8080, weight: 10}

The canary becomes two weighted backends in one rule. The certificate moves to the Gateway’s namespace (or stays put with a ReferenceGrant). The orders namespace needs the gateway-access: public label.

Drill: ingress2gateway on sample manifests

I ran this on 6 October 2026 on a shared VM container: 8 vCPUs, 15 GB RAM, Debian 13, Linux 6.12. Tools: the ingress2gateway v1.2.0 Linux release binary (checksum verified), and a local kube-apiserver v1.37.0 with etcd from the controller-runtime envtest assets, with the Gateway API v1.6.2 Standard CRDs installed. No Gateway controller ran and no traffic was sent, so these results are about translation and API validation only.

Input: three Ingresses on one host. The main one used 9 annotations (body size, read timeout, CORS, IP allow list, limit-rps, auth-url, cookie affinity, a configuration snippet); a canary Ingress used weight 10; a third used use-regex with rewrite-target: /$2.

Check

What I saw

Standard emitter

Exit code 0. One Gateway (HTTP and HTTPS listeners), two HTTPRoutes, 14 warnings and 1 info note

Unsupported annotations

auth-url, limit-rps and configuration-snippet named as unsupported; cookie affinity “not supported”

use-regex on one Ingress

Paths from all three Ingresses became case-insensitive regex, for example (?i)/api.*

rewrite-target: /$2

Not converted (“capture group references are not supported”). The route kept the regex match with no rewrite, so the backend would see the original path

proxy-read-timeout: "60"

timeouts.request: 10m0s. The source takes the largest of the three timeouts and multiplies by 10

Canary weight 10

Backends weighted 10 and 90

envoy-gateway emitter

Added two BackendTrafficPolicies (request buffer 8Mi) and two SecurityPolicies (allow 10.0.0.0/8, default deny)

Host with 20 paths, no TLS

One HTTPRoute with 20 rules. The API server rejected it: “spec.rules: Too many: 20: must have at most 16 items”. Also a warning that ingress-nginx served HTTPS for this host with a self-signed certificate, which is not translated

Experimental CRDs over Standard

Denied by the safe-upgrades policy

HTTPRoute with retry on Standard CRDs

Rejected as an unknown field. With --validate=false, the field was silently dropped

Reading from the API server instead of files

Same objects; only the order of two backends differed

The lesson is not that the tool is weak. Its warnings were specific and honest. The lesson is that a clean exit code does not mean the same behaviour. Apply the output with server-side validation, read every warning, and test traffic.

Migration checklist

  1. Inventory. Controllers, IngressClasses, Ingress count per namespace, annotation counts, ConfigMap, controller flags, and anything that depends on the controller’s IP or X-Forwarded-* headers.
  2. Classify each annotation: Standard equivalent, implementation policy, or behaviour you will drop on purpose. Snippets and auth usually need the most work.
  3. Choose the target using the extended features and policies you need, and your team’s comfort with Envoy, NGINX, HAProxy or Traefik.
  4. Decide CRD ownership. Install Standard channel CRDs once, as the cluster owner, and pin the version.
  5. Run side by side. The Gateway API guide for Ingress-NGINX users recommends this: the new controller gets its own external IP and production stays untouched.
  6. Convert per namespace with ingress2gateway, the emitter for your implementation, and --input-file or -n. Commit the output and the warnings for review.
  7. Fix by hand. Split routes over 16 rules, rewrite regex and capture-group paths, set timeouts deliberately, move certificates or add ReferenceGrants.
  8. Test behaviour. Use curl --resolve against the new IP for real paths, redirects, trailing slashes, mixed-case paths, large uploads, CORS preflight and gRPC. Compare status codes and headers with the old controller.
  9. Shift traffic gradually. Lower DNS TTLs a day early, move one host at a time, and watch errors and latency.
  10. Clean up. Remove ingress-nginx, its admission webhook, IngressClass and RBAC, and the Ingress objects it served, so nothing falls back to it later.

Common mistakes

  • Mixing up the projects. F5’s NGINX Ingress Controller and NGINX Gateway Fabric are maintained. Community ingress-nginx is retired.
  • Treating a clean conversion as done. Warnings and limits like the 16 rule cap only show up when you read and apply the output.
  • Copying timeouts one to one. NGINX read timeouts are per read. Gateway API request timeouts cover the whole request.
  • Forgetting host-wide regex. One use-regex Ingress changed matching for its neighbours.
  • Installing experimental CRDs for one field without accepting that the channel has no compatibility promise.
  • Letting each team install its own Gateway API CRDs through different Helm charts.
  • Leaving the old controller running “just in case” with its cluster-wide access to Secrets.

What to unlearn and re-learn

  • Unlearn “retired means it stops working”. Re-learn that it keeps working without fixes, which is riskier because it is quiet.
  • Unlearn “annotations are configuration”. Re-learn them as controller behaviour that must be mapped, replaced or dropped on purpose.
  • Unlearn “one Ingress object per app is the whole story”. Re-learn routing as a split between platform owned Gateways and team owned routes.
  • Unlearn “the converter migrates for me”. Re-learn it as a draft generator that saves typing and lists the gaps.

Revisit your ingress before you migrate

Ingress NGINX served many of us well, and its retirement is not a reason to panic. It is a reason to look closely at what our edge actually does. Learn the Gateway API resources and channels, unlearn the habit of solving everything with an annotation, re-learn ownership between platform and application teams, practise the conversion on a copy of your manifests, and apply the move one host at a time. An inventory turns a vague worry into a list of concrete items, and the edge you end up with is easier to reason about.

Sources

comments powered by Disqus

Releted Posts

MinIO and its alternatives: revisit your self-hosted S3 in 2026

For many teams, MinIO was the default answer to one simple need: “give us S3 on our own machines”. It sat behind CI pipelines, backup jobs, data lake experiments and almost every docker-compose file that needed a bucket.

Read more

Elasticsearch and OpenSearch: revisit the fork before you pick one

Many teams still talk about “our Elastic cluster” when the cluster is actually Amazon OpenSearch Service, or about “OpenSearch” when half the code paths still use an old Elasticsearch client.

Read more

Kafka and Redpanda: revisit the choice before you treat them as the same bus

Many teams say “we run Kafka” when they mean “our services talk to a Kafka-compatible broker”. That shortcut was useful when one open source broker dominated the protocol.

Read more