In March 2026, Kubernetes retired Ingress-NGINX, the controller that routes outside traffic into a cluster. If you have run Kubernetes in the last few years, you probably ran it, or ran something that depended on it. The repository is now archived and read-only. No more releases, bug fixes or security patches will come.
Nothing broke on that day, and that is the problem. Existing deployments keep working, the Helm charts and container images are still downloadable, and the controller keeps routing traffic. It will keep doing so until the next vulnerability is found, and nobody will be there to fix it.
Ingress-NGINX is the loud case.
The quiet cases are the controllers, operators and admission webhooks in your cluster that nobody has ever counted, let alone checked for a healthy maintainer. This article gives you a method for finding them. First you inventory what runs. Then you score each dependency on four axes. Then you decide whether to fork it, own it or replace it. We also describe the one component we chose to own ourselves, and why.
The event that made this urgent
Retired in March 2026
SIG Network and the Security Response Committee announced the retirement on 11 November 2025. Best-effort maintenance would continue until March 2026. After that, in the announcement's words, there would be no further releases, no bugfixes and no updates to resolve security vulnerabilities that may be discovered. The repository now carries the banner "archived by the owner on Mar 24, 2026. It is now read-only."
The announcement also said that existing deployments would not be broken, and that Helm charts and images would remain available. That reassurance is accurate and slightly dangerous. It means a cluster can run a retired controller for months without a single visible symptom. If you are not sure whether you run it, the announcement gives a one-line check:
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
Roughly half of environments
In January 2026 the Kubernetes Steering Committee and the Security Response Committee issued a joint statement. They called Ingress-NGINX "a piece of critical infrastructure for about half of cloud native environments." They wrote that they could not overstate the severity of the situation, and that it could not be "left until the last minute to address."
The risk is not theoretical, because the project was still producing vulnerabilities in its final weeks. On 9 March 2026 the project published CVE-2026-3288, rated 8.8 (High). The rewrite-target annotation could inject configuration into nginx, which the advisory says can lead to arbitrary code execution in the controller's context and expose the Secrets the controller can read. In a default installation, that means all Secrets in the cluster. Fixes shipped in 1.13.8, 1.14.4 and 1.15.0. Fifteen days later the repository was archived. The next advisory of this kind has no fix waiting for it.
The successor was retired too
The usual exit from an abandoned project is its successor. Here the retirement notice closes that door. The maintainers had announced a wind-down in 2024 and planned a replacement with the Gateway API community, called InGate. Its development, in the notice's words, never progressed far enough to create a mature replacement, and it is being retired as well. The Kubernetes project points users at the Gateway API itself, or at one of the third-party controllers.
Why it happened, and why it will happen again

From announcement to archive, and the three causes behind it. Sources: Kubernetes blog, CVE-2026-3288 advisory, ingress-nginx repository.
Volunteers against a backlog
The retirement notice says that for years the project had only one or two people doing development work, on their own time, after work hours and on weekends. The wind-down announced in 2024 did not attract enough new maintainers, and SIG Network and the Security Response Committee concluded they had exhausted their efforts to find support.
Look at that sentence from the user's side. A controller sitting in the traffic path of about half of cloud native environments depended on the spare time of one or two people. Every company that ran it had, in effect, an unwritten dependency on that spare time. None of those companies wrote it down, because nothing in a kubectl listing shows it.
Flexibility became a liability
The same notice explains what made the project hard to carry. Its snippet annotations allowed arbitrary NGINX configuration. That flexibility made the controller popular, and later came to be seen as serious security flaws. The notice puts it in one line: "Yesterday's flexibility has become today's insurmountable technical debt."
CVE-2026-3288 is the same story in a smaller form. An annotation that rewrites a path becomes a way to write nginx configuration. The Kubernetes project describes Ingress-NGINX the same way: it extends Ingress through "esoteric annotations, ConfigMaps, and CRDs" (Kubernetes blog). A large feature surface needs a large review capacity, and the project never had one.
The pattern repeats
Strip the names away and the story has four ingredients. A component is adopted widely because it solves a real problem. Its maintainer pool stays tiny. Its feature surface grows faster than the reviewers can read it. And nobody funds a successor until the pool runs dry. Any controller or operator can have all four.
You can see the preconditions in other places. The ExternalDNS README states that no new provider is accepted in-tree, lists 38 third-party webhook providers, and warns that its maintainers have not reviewed them and that you use them at your own risk. It also says providers without a maintainer listed are in need of assistance (ExternalDNS README). That is an honest description of how an ecosystem is staffed, and every cluster that depends on it inherits the same exposure.
Building the inventory
If everything in your cluster is installed through GitOps, the inventory already exists: it is the repository. That is how we work. Every component on our clusters goes in through Argo CD, so the list of what runs is a directory listing, and the owner of a directory is the owner of the component. The commands below are for the other case: a cluster you inherited, one that grew by hand before GitOps arrived, or a one-off check that the cluster still matches Git. Everything in this section is read-only kubectl, with jq where shown, so you can run it against production.

Six places a load-bearing component hides, and the command that lists each.
What counts
A dependency is anything that sits in a path your workloads need and that your team did not write. We group them into six kinds.
Controllers. Deployments and DaemonSets in system and platform namespaces that watch API objects and act on them, such as ingress controllers and secret controllers.
Operators and CRDs. Every custom API group on the cluster belongs to a controller. An operator like CloudNativePG is the controller for its group.
Admission webhooks. Validating and mutating webhooks sit in the path of every create and update that matches their rules.
Aggregated APIs. APIs served by a pod behind the API server, metrics being the common example.
CSI drivers and CNI plugins. Storage and pod networking, under every workload.
Out-of-tree providers. Plug-ins that run beside a well-known controller and talk to it over a local interface.
Listing them from a live cluster
Start with the images, because every running component has one. Counting containers rather than pods matters here, for reasons the last subsection explains.
If you run GitOps, finish with a comparison: what is live against what the repository declares. Anything running that Git does not describe is drift. Bring it under GitOps or remove it, so the next inventory is a look at the repository again rather than another investigation.
The ones that hide
The hardest components to find run as a second container in a pod you already trust. ExternalDNS is the clean example. It has stopped taking new providers in-tree, so a provider for a new DNS service ships as a webhook, a separate program that runs as a sidecar and talks to ExternalDNS over localhost. Our own provider's README describes exactly that: it runs next to ExternalDNS, and its API is bound to 127.0.0.1 and consumed only by ExternalDNS.
A pod listing shows external-dns and a green status. The name says nothing about who wrote the second container, how often it is released or whether anyone answers its issues. The same shape appears in cert-manager, which supports out-of-tree DNS providers through an external webhook, and in storage and networking drivers. This is why step 1 counts containers, and why this query is worth running on its own:
Most of what it prints will be ordinary service-mesh and logging sidecars. Read the rows from platform namespaces first, and look for images from registries and organisations you do not recognise.
Scoring each dependency
An inventory of eighty components is a list, not a plan. Scoring turns it into an order of work. We keep the axes to four, because a scorecard with twelve fields does not get filled in. Rate each one low, medium or high.
Axis | Question | Where to look | Red flag |
Blast radius | What stops, and for whom, if this component stops? | Your GitOps repository, the webhook failure policy, what depends on its CRDs | Traffic, storage, networking or a Fail-policy webhook |
Maintenance signal | Is anyone releasing, reviewing and answering security reports? | Repository activity, release dates, published advisories | Archived, no release in a year, one active author |
Replaceability | Is there a drop-in, a tool-assisted migration, or a rewrite? | Project docs, migration guides, other implementations of the same API | Behaviour baked into annotations or private CRDs |
In-house understanding | Can someone on the team read it, reproduce a bug and ship a patch? | A named person, not a team | The honest answer is "we would open an issue" |
Blast radius
Ask what stops, and also how it fails. An ingress controller that is no longer patched keeps serving traffic, so it fails open and quietly: the risk is a vulnerability, not an outage. A validating webhook with a Fail policy fails closed and loudly, because the cluster stops accepting changes. A CSI driver that stops can freeze volume operations while running pods carry on. These are different risks and they call for different urgency, so write both the radius and the failure mode in the table.
Use signals you can pull rather than impressions. Stars are not one of them: the archived Ingress-NGINX repository page shows 19.4k stars. Look at four things.
Archived flag. The loudest signal there is, and a free one to check.
Release cadence. The date of the latest release, and the gap between the last few.
Active authors. The number of distinct people who committed in the last twelve months. One or two is the pattern that retired Ingress-NGINX.
Security response. The published advisories, and how long each took from report to fix. Read two or three of them by hand.
Change the date in the third command to twelve months before you run it. A component with a single active author and a long gap since the last release is not necessarily dead, but it has told you what its bus factor is.
In our setup nobody runs these checks by hand. Updatecli runs in CI against the GitOps repository, compares the versions we pin with what each upstream has released, and opens a pull request when something changes. The team reviews and approves it. A component that should be releasing and never produces such a pull request is worth a closer look, and that is where the commands above help.
Replaceability
Replaceability comes in three grades. A drop-in is another implementation of the same API, where you change the image and keep the objects. A migration moves you to a different API, ideally with a tool that does most of the translation. A rewrite means the behaviour lives in your own configuration and has to be rebuilt. For Ingress-NGINX the grade depends on where you go. Moving to Gateway API is a migration. Moving to Traefik is close to a drop-in: its Kubernetes Ingress NGINX provider reads existing Ingress resources, nginx annotations included, so most configuration stays as it is. The Kubernetes team's own guidance is a useful brake on optimism: a seemingly correct translation can still cause outages if it ignores the old controller's quirks.
In-house understanding
This axis decides whether a problem is an inconvenience or an incident. Ask for a name. Who would read the controller's code at three in the morning, find the cause and be comfortable shipping a change? "We would raise an issue upstream" is a valid process for a healthy project and a red flag for one with one active author.
Putting the axes together
The component to act on first has a high blast radius, a weak maintenance signal and nobody in-house who understands it. Ingress-NGINX on 25 March 2026 scored like this:
Component | Blast radius | Maintenance signal | Replaceability | Understanding |
Ingress-NGINX, 25 March 2026 | High: the traffic path. Fails open | None: archived, no patches | Near drop-in: Traefik with its Ingress NGINX provider. Migration: Gateway API | Yours to fill in |
Your component here |
Run the same scoring across the whole inventory and sort by blast radius first and maintenance signal second. The top of that list is your risk register. It is usually shorter than people expect, and it is almost always different from the list they would have written from memory.
Fork, own, or replace
Once a component is on the list, you have three moves. Ask the questions in this order.
Before any of the three, price the move people skip: upgrading what you already run. How expensive and how hard is it to get onto a supported version, or onto a maintained replacement, compared with patching potential problems yourself for as long as you stay on the old one? Self-patching looks cheaper this month and costs more every month after it, because each fix you carry has to be re-applied, re-tested and remembered. When updates arrive as small pull requests from automation, as they do for us with updatecli, staying current is routine review work, and the comparison usually comes out in favour of the upgrade.

Fork, own or replace. Ask the questions in order, and stop at the first yes.
When forking is a trap
Forking looks like the cheap answer, because the code is already there. In practice it is the most expensive one to run for long. A fork inherits the maintenance burden without the community that used to share it. It drifts, so the next upstream fix no longer applies cleanly. And you now triage the security reports, which means someone on your team must be able to judge a nginx configuration injection bug on its merits.
With Ingress-NGINX the trap has a second layer. A fork carries the whole annotation surface that made the project unmaintainable. Fork a component only when you have someone who understands it, a reason to keep its exact behaviour and a plan for the day you stop. Treat a fork as a bridge, with an end date.
What owning upstream costs
Owning means depending on a project that still lives and taking part in it. The cost is real and it is bounded: a named owner, pinned versions, someone who reads the advisories, a test environment where you can try a release before production does, and a habit of sending fixes upstream instead of patching locally. It is more work at the start and less work forever after, because a fix that lands upstream is a fix you never carry again.
Our own choices put this in perspective. We prefer purpose-built operators, such as CloudNativePG for Postgres and ExternalDNS for DNS, over one generic layer, as we explain in our post on Terraform modules and control planes. We also deliver secrets through Sealed Secrets. Every one of those choices is a dependency, and each deserves the same four questions as anything else. Choosing a focused tool does not remove the audit.
When early replacement wins
Replacing early means you pick the date. Replacing late means the date picks you, usually with a CVE attached. If a migration path exists, take it while the old component still works, run old and new side by side and move traffic over gradually.
For Ingress-NGINX we chose the shortest path we could find, and it was not a rewrite to Gateway API. We moved to Traefik. Since v3.6.2, Traefik ships a provider that watches Ingresses with the nginx ingress class and translates their nginx annotations into its own configuration, so the Ingress objects themselves did not have to change (Traefik migration guide). Almost nothing changed, but not quite nothing. Global ConfigMap keys and snippet annotations such as server-snippet have no direct equivalent, because Traefik does not expose raw nginx internals, so each of those has to be translated by intent.
The migration itself runs side by side: install Traefik next to the old controller with the provider enabled, test it against Traefik's load balancer before touching DNS, shift traffic, then remove the old controller and its admission webhook while keeping the nginx IngressClass. While both controllers watch the same Ingresses they overwrite each other's load balancer status, so the guide recommends turning off status publishing on Traefik for that window.
Gateway API stays the longer-term direction. The point of replacing early is that you pick both the date and the size of the step, and a near drop-in buys time to make the bigger move on purpose.
What we learned from the maintainer's seat
Knowledge from the repository, not a README
A README tells you what a component does when everything works. Maintaining one teaches you what it does when something else fails. Our DigitalOcean provider for ExternalDNS is a small project, and most of its code exists because of those cases. It retries with exponential backoff on rate limits (HTTP 429) and server errors (5xx). It reports recoverable errors as soft errors, so ExternalDNS tries again on the next reconciliation instead of failing the loop. It exports Prometheus metrics for API requests, errors and rate-limit hits.
None of that is exotic. It is simply the difference between code that works and code that has met a real API on a bad afternoon. When you only consume a component, you meet those cases as incidents. When you maintain it, you meet them as pull requests, and you learn to read other projects' code for the same signs.
Why we own our DigitalOcean provider
We maintain the DigitalOcean webhook provider for ExternalDNS, and the reason is the risk calculation this article describes. Our clients run DNS on it. ExternalDNS accepts no new in-tree providers, so a provider like this lives out of tree and sets its own release schedule. Depending on a component we could not fix on our own timeline was the worse option.
The repository says why a webhook was the right shape: an independent release cycle that does not wait for upstream releases, custom rate limiting, retry logic and enhanced error handling. It started from the original in-tree DigitalOcean provider code and was adapted to run standalone, under Apache 2.0. Owning it costs us a release process and attention. It buys us the ability to ship a fix the day a client needs one.
Closing: who understands what you run
Ingress-NGINX did not fail suddenly. It failed slowly, in public, over years, while the people who could see it coming said so. The lesson is a habit: know what is in your cluster, know who keeps it alive, and decide in advance what you will do when they stop.
Keep the inventory where the configuration already lives: in Git. When every component is installed through GitOps, the repository is the inventory and a pull request is the only way a new one gets in. Let automation such as updatecli raise the version bumps, so the team reviews updates instead of hunting for them. If you find yourself running an analysis of the cluster over and over, take it as a sign that something is being installed outside GitOps, and fix that first. Then make the four questions part of the pull request that adds a component. We apply the same thinking when we design a golden path, because the platform you hand developers is only as maintained as its least maintained part.
If you would like a second pair of eyes on your inventory, or help moving off a component that has lost its maintainers, talk to us at amoniac.eu.
SHARE ON SOCIAL MEDIA




