ingress-nginx CVE-2026-116472: No Patch, Block mirror-target
One ingress-nginx annotation can drop all traffic through the controller, and no upstream fix is coming. Block mirror-target with a ValidatingAdmissionPolicy.
One annotation on one Ingress can drop every request through an ingress-nginx controller, and the Kubernetes project will not patch it. CVE-2026-116472, published on 5 November 2026, lets a user who can create an Ingress point the mirror-target annotation at the controller’s own unauthenticated configuration API. If you still run ingress-nginx, block that annotation at the API server today with a ValidatingAdmissionPolicy, then audit what is already stored.
What CVE-2026-116472 breaks
The CVE record, issued by the Kubernetes CNA (CVE Numbering Authority), says it plainly:
Due to a missing check, the mirror-target Ingress annotation can be used to target an unauthenticated configuration API within the ingress-nginx controller, allowing write commands to be submitted.
The result is a full data plane outage for that controller: “all traffic through ingress-nginx being dropped until an Ingress configuration change or a controller restart causes the data to be recreated correctly.” The record also mentions possible redirection of other Ingresses’ traffic, and calls the extra risk “negligible.”
The CNA scores it CVSS 3.1 2.7, Low (AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L). The low score comes from PR:H: the attacker needs Ingress create rights. In a shared cluster, that often includes every team that owns a namespace.
Why no patch is coming
The Kubernetes advisory issue #143852 is labeled cve/wontfix:
Kubernetes will not be issuing a fix for this CVE, because ingress-nginx maintenance ended in March 2026.
That end date was announced a year earlier. The November 2025 retirement post promised “no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered.”
The CVE record lists every upstream ingress-nginx version as affected. It also lists SUSE’s Rancher Prime RKE2 ingress-nginx as affected below 1.15.2-prime1, which is the fixed build for that fork.
How a mirror annotation reaches a localhost API
The mirror annotation docs describe a traffic-shadowing feature: “Enables a request to be mirrored to a mirror backend. Responses by mirror backends are ignored.” The value is a URL, for example https://test.env.com$request_uri.
The controller turns that URL into nginx config. In template.go, buildMirrorLocations writes the annotation value straight into a proxy_pass:
buffer.WriteString(fmt.Sprintf(`location = %v {
internal;
proxy_set_header Host %v;
proxy_pass %v;
}
`, strconv.Quote(loc.Mirror.Source), strconv.Quote(loc.Mirror.Host), strconv.Quote(loc.Mirror.Target)))
The same nginx process runs a status server, defined in nginx.tmpl, that listens only on localhost and has no authentication:
listen 127.0.0.1:{{ .StatusPort }};
...
location /configuration {
content_by_lua_file /etc/nginx/lua/nginx/ngx_conf_configuration.lua;
}
StatusPort is 10246 in internal/nginx/main.go. The controller pushes routing data there with POST /configuration/backends, and configuration.lua stores the body in a shared dictionary that the balancer reads for every request.
“Localhost only” was the access control. A proxy that can be told to fetch localhost URLs on behalf of a tenant removes it. The advisory gives the matching detection hint: “Localhost addresses in the mirror-target annotation of an Ingress resource may indicate an attempt” at exploitation.
Pick a mitigation that fits your cluster
The advisory lists two mitigations before upgrading: an admission controller that rejects Ingresses with mirror-target, or the controller’s annotations-risk-level ConfigMap setting. They differ a lot in what else they break.
The ConfigMap docs set the default to High and explain the rule: “If the risk is, for instance Medium, annotations with risk High and Critical will not be accepted.” The annotation risk table rates mirror-target as High, alongside auth-url and auth-signin. Dropping to Medium blocks the mirror and every OAuth2 proxy setup that uses those two.
| Mitigation | Annotations blocked | Where it acts | Also blocks |
|---|---|---|---|
ValidatingAdmissionPolicy on */mirror-target |
1 | API server, before storage | Nothing else |
annotations-risk-level: "Medium" |
15 (Critical and High) | Controller ConfigMap | auth-url, auth-signin, all snippets |
annotations-risk-level: "Low" |
60 (Critical, High, Medium) | Controller ConfigMap | Also rewrite-target, proxy-body-size |
The admission policy is the narrow cut. ValidatingAdmissionPolicy is built into the API server and has been generally available since Kubernetes 1.30, so it needs no webhook to run or keep healthy.
Block mirror-target with a ValidatingAdmissionPolicy
The policy matches any key that ends in /mirror-target. That covers controllers started with a custom --annotations-prefix instead of the default nginx.ingress.kubernetes.io.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: deny-ingress-mirror-target
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["networking.k8s.io"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["ingresses"]
validations:
- expression: >-
!has(object.metadata.annotations) ||
!object.metadata.annotations.exists(k, k.endsWith('/mirror-target'))
message: "mirror-target is blocked (CVE-2026-116472)"
reason: Forbidden
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: deny-ingress-mirror-target
spec:
policyName: deny-ingress-mirror-target
validationActions: ["Warn"]
Start with Warn, then switch to Deny. Running it against kube-apiserver v1.37.2 in a sandbox, with user dev holding Ingress rights in namespace team-a, shows each stage.
Before the policy, the Ingress goes straight in:
$ kubectl --user=dev apply -f ing-mirror.yaml
ingress.networking.k8s.io/shop-mirror created
With the binding in Warn mode, it still goes in, and the client sees why it will fail later:
$ kubectl --user=dev apply -f ing-mirror.yaml
Warning: Validation failed for ValidatingAdmissionPolicy 'deny-ingress-mirror-target' with binding 'deny-ingress-mirror-target': mirror-target is blocked (CVE-2026-116472)
ingress.networking.k8s.io/shop-mirror created
With validationActions: ["Deny"]:
$ kubectl --user=dev apply -f ing-mirror.yaml
Error from server (Forbidden): error when creating "ing-mirror.yaml": ingresses.networking.k8s.io "shop-mirror" is forbidden: ValidatingAdmissionPolicy 'deny-ingress-mirror-target' with binding 'deny-ingress-mirror-target' denied request: mirror-target is blocked (CVE-2026-116472)
$ kubectl --user=dev apply -f ing-plain.yaml
ingress.networking.k8s.io/shop created
In this run, a new binding took a few seconds to start rejecting requests. Test with a throwaway Ingress after you apply it, not right away.
The last two rows matter during rollout. An Ingress that already had mirror-target stays in etcd. Every later update to it, even a label, is rejected until someone removes the annotation, which is allowed:
$ kubectl --user=dev -n team-a label ingress shop-mirror owner=payments
Error from server (Forbidden): ingresses.networking.k8s.io "shop-mirror" is forbidden: ValidatingAdmissionPolicy 'deny-ingress-mirror-target' with binding 'deny-ingress-mirror-target' denied request: mirror-target is blocked (CVE-2026-116472)
$ kubectl --user=dev -n team-a annotate ingress shop-mirror nginx.ingress.kubernetes.io/mirror-target-
ingress.networking.k8s.io/shop-mirror annotated
Audit the Ingresses you already have
Find every stored mirror-target, with its value. Treat any 127.0.0.1, localhost or [::1] target as an incident:
kubectl get ingress -A -o json \
| jq -r '.items[] | .metadata as $m | ($m.annotations // {}) | to_entries[]
| select(.key | endswith("/mirror-target"))
| [$m.namespace, $m.name, .value] | @tsv'
team-a shop-mirror http://127.0.0.1$request_uri
If you are weighing annotations-risk-level: "Medium" instead, list which High and Critical annotations are in use first. This joins the live Ingresses with the upstream risk table:
curl -s https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/docs/user-guide/nginx-configuration/annotations-risk.md \
| awk -F'|' '$4 ~ /High|Critical/ {gsub(/ /, "", $3); print $3}' > high-risk.txt
kubectl get ingress -A -o json \
| jq -r '.items[] | .metadata as $m | ($m.annotations // {}) | keys[]
| select(startswith("nginx.ingress.kubernetes.io/"))
| "\($m.namespace)/\($m.name)\t\(sub("^nginx.ingress.kubernetes.io/"; ""))"' \
| awk -F'\t' 'NR==FNR {high[$1]; next} $2 in high' high-risk.txt -
team-a/shop-mirror auth-url
Any line in that output is an Ingress that Medium would break.
What to do
- Find out if you run it:
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx, the check from the retirement post. Run it in every cluster. - Run the
mirror-targetaudit above. Remove the annotation where nobody needs traffic mirroring, and report localhost targets to your security team and to [email protected], as the advisory asks. - Apply the ValidatingAdmissionPolicy with
validationActions: ["Warn"], watch for warnings in CI and kubectl output for a day, then switch to["Deny"]and confirm a test Ingress is rejected. - On Rancher Prime RKE2, upgrade the ingress-nginx fork to
1.15.2-prime1or later. On other managed platforms, check your vendor’s guidance. - Plan the move off ingress-nginx. The project ships no more security fixes, and the retirement post recommends Gateway API or another Ingress controller.