---
id: "CVE-2026-62290"
kind: "vulnerability"
aliases: ["CVE-2026-62290", "GHSA-8rvj-mm4h-c258"]
title: "cert-manager: Direct ACME Challenge resources can bypass Issuer DNS01 solver policy and use ClusterIssuer DNS credentials"
severity: "high"
cvss: 7.3
epss: 0.00112
exploited: false
status: "open"
category: "infrastructure"
products: [{"name": "github.com/cert-manager/cert-manager", "fixed": ["1.19.6"], "affected": ">= 1.18.0, < 1.19.6", "ecosystem": "go"}, {"name": "github.com/cert-manager/cert-manager", "fixed": ["1.20.3"], "affected": ">= 1.20.0-alpha.0, < 1.20.3", "ecosystem": "go"}, {"name": "linuxfoundation cert-manager", "fixed": ["1.19.6"], "affected": ">= 1.18.0, < 1.19.6", "ecosystem": "vendor"}, {"name": "linuxfoundation cert-manager", "fixed": ["1.20.3"], "affected": ">= 1.20.0, < 1.20.3", "ecosystem": "vendor"}]
ecosystems: ["go", "vendor"]
tags: ["acme", "acme-dns", "cert-manager", "cwe-863", "dns01", "infrastructure", "kubernetes", "multi-tenant"]
not_affected_if: ["default-rbac-only-platform-admins", "issuer-only-no-clusterissuer", "managed-hosting", "no-clusterissuer-dns01"]
published_at: "2026-07-16T20:16:46+00:00"
first_seen_at: "2026-10-29T16:44:11+00:00"
updated_at: "2026-10-29T16:56:32+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-8rvj-mm4h-c258", "first_seen_at": "2026-10-29T16:44:11+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62290", "first_seen_at": "2026-10-29T16:56:32+00:00"}]
---

# cert-manager: Direct ACME Challenge resources can bypass Issuer DNS01 solver policy and use ClusterIssuer DNS credentials

A namespace user who can create ACME Challenge resources can supply their own DNS solver configuration, which cert-manager then uses with the ClusterIssuer's DNS credentials. With acme-dns, the attacker-chosen host receives the referenced account credentials as HTTP headers, and the Issuer's DNS name policy can be bypassed.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/cert-manager/cert-manager affected >= 1.18.0, < 1.19.6 fixed 1.19.6
- go:github.com/cert-manager/cert-manager affected >= 1.20.0-alpha.0, < 1.20.3 fixed 1.20.3
- vendor:linuxfoundation cert-manager affected >= 1.18.0, < 1.19.6 fixed 1.19.6
- vendor:linuxfoundation cert-manager affected >= 1.20.0, < 1.20.3 fixed 1.20.3

## How to check

- Run `kubectl get deployment -A -o jsonpath='{.items[*].spec.template.spec.containers[*].image}'` and look for cert-manager images below `1.19.6` or in the `1.20.0-alpha.0` to `1.20.2` range.
- Run `kubectl auth can-i create challenges.acme.cert-manager.io -n <namespace> --as=<user>` for namespace users to see who can create direct Challenges.
- Check whether any ClusterIssuer uses a DNS01 solver with a Secret or ambient credentials: `kubectl get clusterissuer -o yaml`.

## Fix

- cert-manager `1.18.x`: move to `1.19.6`
- cert-manager `1.19.x`: move to `1.19.6`
- cert-manager `1.20.x`: move to `1.20.3`

## Timeline

- 2026-10-29T16:44:11+00:00 ghsa https://github.com/advisories/GHSA-8rvj-mm4h-c258
- 2026-10-29T16:56:32+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-62290

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/cert-manager/cert-manager/security-advisories/GHSA-8rvj-mm4h-c258
- [advisory] Maintainer's advisory: github.com https://github.com/cert-manager/cert-manager/security/advisories/GHSA-8rvj-mm4h-c258
- [fix] Patch: github.com https://github.com/cert-manager/cert-manager/pull/8940
- [fix] Patch: github.com https://github.com/cert-manager/cert-manager/pull/8941
- [fix] Patch: github.com https://github.com/cert-manager/cert-manager/commit/6bda47297c8fbc6b121b8b76624b668d26f1a155
- [fix] Patch: github.com https://github.com/cert-manager/cert-manager/commit/b37dbf01ecea50a0b3a19df0a7fe4c5ad6803f16
- [fix] Release: github.com https://github.com/cert-manager/cert-manager/releases/tag/v1.19.6
- [fix] Release: github.com https://github.com/cert-manager/cert-manager/releases/tag/v1.20.3

## From the sources

## Summary

`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.io` resources in at least one namespace.
- The cert-manager controller is running and reconciles ACME `Challenge` resources cluster-wide.
- A usable `ClusterIssuer` exists in the cluster.
- The referenced `ClusterIssuer` has a DNS01 solver that uses a Secret in the cluster resource namespace, or relies on ambient credentials enabled for `ClusterIssuer`.
- 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:

- `master` commit `2c0f1a6a4`
- latest production release `v1.20.2` commit `e5b7b1845`
- `v1.19.5` commit `cda7b7896`
- `v1.18.6` commit `4699caabb`

See `VERSION_TEST_MATRIX.md` for the command matrix and outputs.

## Technical details

Relevant code paths:

- `internal/apis/acme/validation/challenge.go:44-45`: `ValidateChallenge` returns no errors on create.
- `deploy/crds/acme.cert-manager.io_challenges.yaml`: `spec.solver` is a required user-controlled field on `Challenge`.
- `pkg/controller/acmechallenges/scheduler/scheduler.go:51-58`: scheduler lists all Challenges.
- `pkg/controller/acmechallenges/controller.go:180-197`: scheduler sets `status.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 solver `Present` ignores the issuer object argument.
- `pkg/issuer/acme/dns/dns.go:180-187`: DNS provider config and resource namespace are derived from `Challenge.spec`.
- `pkg/controller/helper.go:39-44`: `ClusterIssuer` references use the cluster resource namespace.
- `pkg/controller/helper.go:66-72`: ambient credentials are allowed based on `issuerRef.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/update `challenges` and `orders`.

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:

```bash
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: admin@example.com
    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
   

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
