VYPR
Moderate severityNVD Advisory· Published Feb 4, 2026· Updated Feb 5, 2026

cert-manager-controller DoS via Specially Crafted DNS Response

CVE-2026-25518

Description

cert-manager adds certificates and certificate issuers as resource types in Kubernetes clusters, and simplifies the process of obtaining, renewing and using those certificates. In versions from 1.18.0 to before 1.18.5 and from 1.19.0 to before 1.19.3, the cert-manager-controller performs DNS lookups during ACME DNS-01 processing (for zone discovery and propagation self-checks). By default, these lookups use standard unencrypted DNS. An attacker who can intercept and modify DNS traffic from the cert-manager-controller pod can insert a crafted entry into cert-manager's DNS cache. Accessing this entry will trigger a panic, resulting in denial‑of‑service (DoS) of the cert-manager controller. The issue can also be exploited if the authoritative DNS server for the domain being validated is controlled by a malicious actor. This issue has been patched in versions 1.18.5 and 1.19.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

cert-manager controller's unencrypted DNS lookups during ACME DNS-01 processing allow attackers to inject crafted responses, causing a panic and denial of service; patched in versions 1.18.5 and 1.19.3.

Vulnerability

Overview

CVE-2026-25518 is a denial-of-service vulnerability in the cert-manager controller, affecting versions 1.18.0 through 1.18.4 and 1.19.0 through 1.19.2. The root cause is that the controller performs DNS lookups without encryption during ACME DNS-01 challenge processing, specifically for zone discovery and propagation self-checks. An attacker who can intercept and modify DNS traffic from the cert-manager-controller pod can inject a crafted entry into the controller's DNS cache, which upon access triggers a panic and crashes the controller [1][2].

Exploitation

Prerequisites

Exploitation requires the attacker to be in a position to intercept or manipulate DNS responses reaching the cert-manager-controller pod. This could be achieved by compromising the network path (e.g., via a malicious DNS resolver or man-in-the-middle attack) or by controlling the authoritative DNS server for the domain being validated. No authentication is needed beyond the ability to influence DNS traffic [2].

Impact

A successful attack causes the cert-manager controller to panic, resulting in a denial of service. This disrupts the controller's ability to issue, renew, or manage TLS certificates, potentially leading to certificate expiry and service outages in the Kubernetes cluster [2].

Mitigation

The issue has been patched in cert-manager versions 1.18.5 and 1.19.3. Users running affected versions should upgrade immediately to the latest patched release. No workarounds are documented; the fix addresses the unencrypted DNS lookups to prevent the injection of malicious responses [1][3].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/cert-manager/cert-managerGo
>= 1.18.0, < 1.18.51.18.5
github.com/cert-manager/cert-managerGo
>= 1.19.0, < 1.19.31.19.3

Affected products

2
  • Range: >=1.18.0,<1.18.5 || >=1.19.0,<1.19.3
  • cert-manager/cert-managerv5
    Range: >= 1.18.0, < 1.18.5

Patches

3
409fc24e5397

Merge pull request #8469 from SgtCoDFish/fqdn-patch

https://github.com/cert-manager/cert-managercert-manager-prow[bot]Feb 2, 2026via ghsa
2 files changed · +119 1
  • pkg/issuer/acme/dns/util/fqdn_test.go+111 0 added
    @@ -0,0 +1,111 @@
    +/*
    +Copyright 2025 The cert-manager Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package util
    +
    +import (
    +	"fmt"
    +	"net"
    +	"testing"
    +
    +	"github.com/miekg/dns"
    +)
    +
    +// This file contains code adapted from a contribution sent by Oleh Konko as part of GHSA-gx3x-vq4p-mhhv
    +
    +func Test_FindZoneByFqdn_NoPanic(t *testing.T) {
    +	zone := "example.com."
    +	fqdn := fmt.Sprintf("findzonebyfqdn.%s", zone)
    +
    +	// start the dummy DNS server which we'll query
    +	ns, stop := startDNS(t, zone)
    +	defer stop()
    +
    +	// First call to FindZoneByFqdn to populate the cache
    +	_, err := FindZoneByFqdn(t.Context(), fqdn, []string{ns})
    +	if err != nil {
    +		t.Fatalf("first call too FindZoneByFqdn failed: %v", err)
    +	}
    +
    +	// We want to test that the second call does not panic; catch a panic here for prettier log output
    +
    +	defer func() {
    +		r := recover()
    +		if r != nil {
    +			t.Fatalf("got a panic but none expected: %v", r)
    +		}
    +	}()
    +
    +	// Second call to FindZoneByFqdn should find the SOA record in the cached response without panic
    +
    +	_, err = FindZoneByFqdn(t.Context(), fqdn, []string{ns})
    +	if err != nil {
    +		t.Fatalf("second call too FindZoneByFqdn failed: %v", err)
    +	}
    +}
    +
    +// startDNS starts a local DNS server that responds with a fixed SOA record for any query
    +func startDNS(t *testing.T, zone string) (addr string, stop func()) {
    +	t.Helper()
    +
    +	lc := &net.ListenConfig{}
    +
    +	pc, err := lc.ListenPacket(t.Context(), "udp", "127.0.0.1:0")
    +	if err != nil {
    +		t.Fatalf("failed to listen udp: %v", err)
    +	}
    +
    +	h := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
    +		m := new(dns.Msg)
    +		m.SetReply(r)
    +		m.Authoritative = true
    +
    +		qname := zone
    +		if len(r.Question) > 0 {
    +			qname = r.Question[0].Name
    +		}
    +
    +		// this is specially crafted: the SOA record exists but is not at Answer[0]
    +		m.Answer = []dns.RR{
    +			&dns.NS{
    +				Hdr: dns.RR_Header{Name: qname, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 600},
    +				Ns:  "ns1.example.com.",
    +			},
    +			&dns.SOA{
    +				Hdr:     dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 600},
    +				Ns:      "ns1.example.com.",
    +				Mbox:    "hostmaster.example.com.",
    +				Serial:  1,
    +				Refresh: 3600,
    +				Retry:   600,
    +				Expire:  86400,
    +				Minttl:  60,
    +			},
    +		}
    +
    +		_ = w.WriteMsg(m)
    +	})
    +
    +	srv := &dns.Server{PacketConn: pc, Handler: h}
    +	go func() {
    +		_ = srv.ActivateAndServe()
    +	}()
    +
    +	return pc.LocalAddr().String(), func() {
    +		_ = srv.ShutdownContext(t.Context())
    +		_ = pc.Close()
    +	}
    +}
    
  • pkg/issuer/acme/dns/util/wait.go+8 1 modified
    @@ -312,7 +312,14 @@ func FindZoneByFqdn(ctx context.Context, fqdn string, nameservers []string) (str
     		// ensure cachedEntry is not expired
     		if time.Now().Before(cachedEntryItem.ExpiryTime) {
     			logf.FromContext(ctx).V(logf.DebugLevel).Info("Returning cached DNS response", "fqdn", fqdn)
    -			return cachedEntryItem.Response.Answer[0].(*dns.SOA).Hdr.Name, nil
    +
    +			for _, ans := range cachedEntryItem.Response.Answer {
    +				if soa, ok := ans.(*dns.SOA); ok {
    +					return soa.Hdr.Name, nil
    +				}
    +			}
    +
    +			return "", fmt.Errorf("cached response has no SOA record")
     		}
     
     		// Remove expired entry
    
d4faed26ae12

Merge pull request #8468 from SgtCoDFish/release-1.19-fqdn-patch

https://github.com/cert-manager/cert-managercert-manager-prow[bot]Feb 2, 2026via ghsa
2 files changed · +119 1
  • pkg/issuer/acme/dns/util/fqdn_test.go+111 0 added
    @@ -0,0 +1,111 @@
    +/*
    +Copyright 2025 The cert-manager Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package util
    +
    +import (
    +	"fmt"
    +	"net"
    +	"testing"
    +
    +	"github.com/miekg/dns"
    +)
    +
    +// This file contains code adapted from a contribution sent by Oleh Konko as part of GHSA-gx3x-vq4p-mhhv
    +
    +func Test_FindZoneByFqdn_NoPanic(t *testing.T) {
    +	zone := "example.com."
    +	fqdn := fmt.Sprintf("findzonebyfqdn.%s", zone)
    +
    +	// start the dummy DNS server which we'll query
    +	ns, stop := startDNS(t, zone)
    +	defer stop()
    +
    +	// First call to FindZoneByFqdn to populate the cache
    +	_, err := FindZoneByFqdn(t.Context(), fqdn, []string{ns})
    +	if err != nil {
    +		t.Fatalf("first call too FindZoneByFqdn failed: %v", err)
    +	}
    +
    +	// We want to test that the second call does not panic; catch a panic here for prettier log output
    +
    +	defer func() {
    +		r := recover()
    +		if r != nil {
    +			t.Fatalf("got a panic but none expected: %v", r)
    +		}
    +	}()
    +
    +	// Second call to FindZoneByFqdn should find the SOA record in the cached response without panic
    +
    +	_, err = FindZoneByFqdn(t.Context(), fqdn, []string{ns})
    +	if err != nil {
    +		t.Fatalf("second call too FindZoneByFqdn failed: %v", err)
    +	}
    +}
    +
    +// startDNS starts a local DNS server that responds with a fixed SOA record for any query
    +func startDNS(t *testing.T, zone string) (addr string, stop func()) {
    +	t.Helper()
    +
    +	lc := &net.ListenConfig{}
    +
    +	pc, err := lc.ListenPacket(t.Context(), "udp", "127.0.0.1:0")
    +	if err != nil {
    +		t.Fatalf("failed to listen udp: %v", err)
    +	}
    +
    +	h := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
    +		m := new(dns.Msg)
    +		m.SetReply(r)
    +		m.Authoritative = true
    +
    +		qname := zone
    +		if len(r.Question) > 0 {
    +			qname = r.Question[0].Name
    +		}
    +
    +		// this is specially crafted: the SOA record exists but is not at Answer[0]
    +		m.Answer = []dns.RR{
    +			&dns.NS{
    +				Hdr: dns.RR_Header{Name: qname, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 600},
    +				Ns:  "ns1.example.com.",
    +			},
    +			&dns.SOA{
    +				Hdr:     dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 600},
    +				Ns:      "ns1.example.com.",
    +				Mbox:    "hostmaster.example.com.",
    +				Serial:  1,
    +				Refresh: 3600,
    +				Retry:   600,
    +				Expire:  86400,
    +				Minttl:  60,
    +			},
    +		}
    +
    +		_ = w.WriteMsg(m)
    +	})
    +
    +	srv := &dns.Server{PacketConn: pc, Handler: h}
    +	go func() {
    +		_ = srv.ActivateAndServe()
    +	}()
    +
    +	return pc.LocalAddr().String(), func() {
    +		_ = srv.ShutdownContext(t.Context())
    +		_ = pc.Close()
    +	}
    +}
    
  • pkg/issuer/acme/dns/util/wait.go+8 1 modified
    @@ -312,7 +312,14 @@ func FindZoneByFqdn(ctx context.Context, fqdn string, nameservers []string) (str
     		// ensure cachedEntry is not expired
     		if time.Now().Before(cachedEntryItem.ExpiryTime) {
     			logf.FromContext(ctx).V(logf.DebugLevel).Info("Returning cached DNS response", "fqdn", fqdn)
    -			return cachedEntryItem.Response.Answer[0].(*dns.SOA).Hdr.Name, nil
    +
    +			for _, ans := range cachedEntryItem.Response.Answer {
    +				if soa, ok := ans.(*dns.SOA); ok {
    +					return soa.Hdr.Name, nil
    +				}
    +			}
    +
    +			return "", fmt.Errorf("cached response has no SOA record")
     		}
     
     		// Remove expired entry
    
9a73a0b38530

Merge pull request #8467 from SgtCoDFish/release-1.18-fqdn-patch

https://github.com/cert-manager/cert-managercert-manager-prow[bot]Feb 2, 2026via ghsa
2 files changed · +119 1
  • pkg/issuer/acme/dns/util/fqdn_test.go+111 0 added
    @@ -0,0 +1,111 @@
    +/*
    +Copyright 2025 The cert-manager Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package util
    +
    +import (
    +	"fmt"
    +	"net"
    +	"testing"
    +
    +	"github.com/miekg/dns"
    +)
    +
    +// This file contains code adapted from a contribution sent by Oleh Konko as part of GHSA-gx3x-vq4p-mhhv
    +
    +func Test_FindZoneByFqdn_NoPanic(t *testing.T) {
    +	zone := "example.com."
    +	fqdn := fmt.Sprintf("findzonebyfqdn.%s", zone)
    +
    +	// start the dummy DNS server which we'll query
    +	ns, stop := startDNS(t, zone)
    +	defer stop()
    +
    +	// First call to FindZoneByFqdn to populate the cache
    +	_, err := FindZoneByFqdn(t.Context(), fqdn, []string{ns})
    +	if err != nil {
    +		t.Fatalf("first call too FindZoneByFqdn failed: %v", err)
    +	}
    +
    +	// We want to test that the second call does not panic; catch a panic here for prettier log output
    +
    +	defer func() {
    +		r := recover()
    +		if r != nil {
    +			t.Fatalf("got a panic but none expected: %v", r)
    +		}
    +	}()
    +
    +	// Second call to FindZoneByFqdn should find the SOA record in the cached response without panic
    +
    +	_, err = FindZoneByFqdn(t.Context(), fqdn, []string{ns})
    +	if err != nil {
    +		t.Fatalf("second call too FindZoneByFqdn failed: %v", err)
    +	}
    +}
    +
    +// startDNS starts a local DNS server that responds with a fixed SOA record for any query
    +func startDNS(t *testing.T, zone string) (addr string, stop func()) {
    +	t.Helper()
    +
    +	lc := &net.ListenConfig{}
    +
    +	pc, err := lc.ListenPacket(t.Context(), "udp", "127.0.0.1:0")
    +	if err != nil {
    +		t.Fatalf("failed to listen udp: %v", err)
    +	}
    +
    +	h := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
    +		m := new(dns.Msg)
    +		m.SetReply(r)
    +		m.Authoritative = true
    +
    +		qname := zone
    +		if len(r.Question) > 0 {
    +			qname = r.Question[0].Name
    +		}
    +
    +		// this is specially crafted: the SOA record exists but is not at Answer[0]
    +		m.Answer = []dns.RR{
    +			&dns.NS{
    +				Hdr: dns.RR_Header{Name: qname, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 600},
    +				Ns:  "ns1.example.com.",
    +			},
    +			&dns.SOA{
    +				Hdr:     dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 600},
    +				Ns:      "ns1.example.com.",
    +				Mbox:    "hostmaster.example.com.",
    +				Serial:  1,
    +				Refresh: 3600,
    +				Retry:   600,
    +				Expire:  86400,
    +				Minttl:  60,
    +			},
    +		}
    +
    +		_ = w.WriteMsg(m)
    +	})
    +
    +	srv := &dns.Server{PacketConn: pc, Handler: h}
    +	go func() {
    +		_ = srv.ActivateAndServe()
    +	}()
    +
    +	return pc.LocalAddr().String(), func() {
    +		_ = srv.ShutdownContext(t.Context())
    +		_ = pc.Close()
    +	}
    +}
    
  • pkg/issuer/acme/dns/util/wait.go+8 1 modified
    @@ -312,7 +312,14 @@ func FindZoneByFqdn(ctx context.Context, fqdn string, nameservers []string) (str
     		// ensure cachedEntry is not expired
     		if time.Now().Before(cachedEntryItem.ExpiryTime) {
     			logf.FromContext(ctx).V(logf.DebugLevel).Info("Returning cached DNS response", "fqdn", fqdn)
    -			return cachedEntryItem.Response.Answer[0].(*dns.SOA).Hdr.Name, nil
    +
    +			for _, ans := range cachedEntryItem.Response.Answer {
    +				if soa, ok := ans.(*dns.SOA); ok {
    +					return soa.Hdr.Name, nil
    +				}
    +			}
    +
    +			return "", fmt.Errorf("cached response has no SOA record")
     		}
     
     		// Remove expired entry
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.