Skip to content

Unable to update one of my subdomains since 2.11.4 #7831

Description

@inkanyamba

Issue Details

I wasn't able to access one of my subdomains after updating to 2.11.4.
Debugged with Claude so not sure how accurate the issue is but debugged for a longer than I'd care to admit before finally going back to 2.11.3.

Perhaps some literal embodied quote characters causing some issues. e.g. "OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw" instead of OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw

Hope I don't get crucified for opening up this issue but just in case anyone else faces the same issue hopefully this helps. Below is a dump of the write up Claude did regarding the issue... I went through it's basically the same as everything I'd done with it during my attempt to debug. Hope it helps with getting a patch for the issue.

Summary

When Caddy creates the _acme-challenge TXT record for a DNS-01 challenge via this module, the record's content field on Cloudflare ends up with literal double-quote characters wrapped around the actual token value, e.g.:

"OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw"

instead of the expected:

OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw

Because Caddy's own propagation check (certmagic.solverWrapper) compares the literal expected token against what it resolves over DNS, this mismatch means the check can never succeed, regardless of resolver used or how long propagation_timeout is set. Every obtain/renew attempt fails after the timeout with:

solving challenges: waiting for solver certmagic.solverWrapper to be ready:
timed out waiting for record to fully propagate; verify DNS provider
configuration is correct

Environment

  • Caddy version: 2.11.4 (image: ghcr.io/caddybuilds/caddy-cloudflare:2.11.4)
  • Module version: dns.providers.cloudflare v0.2.4 (per caddy list-modules --versions)
  • DNS provider: Cloudflare (single API token, Zone:DNS:Edit + Zone:Read scoped)
  • Caddyfile relevant snippet:
caddyfile
  (cf_dns) {
      tls {
          dns cloudflare {env.CLOUDFLARE_API_TOKEN}
          resolvers 1.1.1.1 1.0.0.1
      }
  }

  example.sub.yourdomain.com {
      import cf_dns
      reverse_proxy 127.0.0.1:1234
  }

Steps to reproduce

  1. Configure a site with the Cloudflare DNS-01 challenge as above.
  2. Trigger a certificate obtain (e.g. fresh container start for a domain with no existing cert).
  3. While the attempt is in progress, query Cloudflare's API directly for the created record:
   curl -s "https://api.cloudflare.com/client/v4/zones/<zone_id>/dns_records?type=TXT&name=_acme-challenge.example.sub.yourdomain.com" \
     -H "Authorization: Bearer <token>"
  1. Observe the content field includes literal \" characters wrapping the value.

Evidence

For comparison, here are three TXT records that existed simultaneously at the same name (_acme-challenge.<subdomain>.yourdomain.com) during debugging:

Source | content field (raw, as returned by Cloudflare API) | ttl -- | -- | -- Manual curl -X POST (plain content, no quotes in payload) | manual-test-value-12345 | 120 Caddy ACME attempt #1 | "OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw" | 1 Caddy ACME attempt #2 | "F3v_kA6KMzm88n5gooov9aH26bzKwhVu7q7SepNQQY0" | 1

The manually-created record (content submitted with no surrounding quotes) was stored and returned by Cloudflare exactly as submitted — no quotes added. Both Caddy-created records, from the same token and same zone, came back with literal quote characters baked into the content.

This was not a propagation/caching/resolver issue — ruled out by:

  • Setting explicit resolvers 1.1.1.1 1.0.0.1 in the tls block (no change in behavior)
  • Querying the zone's authoritative nameserver directly (jake.ns.cloudflare.com) during an active attempt — record never matched because of the quote mismatch, not because it was absent
  • Confirming via the Cloudflare API directly that the record existed in the zone the entire time, just with malformed content

Expected behavior

The TXT record content written to Cloudflare should be the raw challenge token, with no additional quote characters, matching what a manual API call with the same payload produces.

Actual behavior

The module (or an underlying dependency, possibly related to the recent libdns 1.0 API migration) appears to be double-wrapping the content value in quotes before submitting it to Cloudflare's API, resulting in a permanently broken DNS-01 challenge.

Regression window / workaround

This appears to be a regression introduced between the module version bundled with Caddy 2.11.3 and 2.11.4. Pinning to the 2.11.3-based image resolves the issue completely with an otherwise identical Caddyfile and Cloudflare token. I have not yet identified the exact caddy-dns/cloudflare (or libdns/cloudflare) version bundled with the 2.11.3 build for direct comparison — happy to pull that if useful for triage.

Additional context

Cloudflare's own API/dashboard does have a known general quirk around auto-wrapping TXT content in quotes in some circumstances (acknowledged in their own UI warning, and reported against several other tools: terraform-provider-cloudflare#4650, certbot#10048). That said, the fact that a manually-submitted unquoted payload was stored unquoted, while the module's own writes were quoted, suggests the quoting is happening on the request-construction side here rather than purely a Cloudflare-side inconsistency — but I'm not certain of the exact mechanism without digging into the module's source.

Summary

When Caddy creates the _acme-challenge TXT record for a DNS-01 challenge via this module, the record's content field on Cloudflare ends up with literal double-quote characters wrapped around the actual token value, e.g.:

"OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw"

instead of the expected:

OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw

Because Caddy's own propagation check (certmagic.solverWrapper) compares the literal expected token against what it resolves over DNS, this mismatch means the check can never succeed, regardless of resolver used or how long propagation_timeout is set. Every obtain/renew attempt fails after the timeout with:

solving challenges: waiting for solver certmagic.solverWrapper to be ready:
timed out waiting for record to fully propagate; verify DNS provider
configuration is correct

Environment

Caddy version: 2.11.4 (image: ghcr.io/caddybuilds/caddy-cloudflare:2.11.4)
Module version: dns.providers.cloudflare v0.2.4 (per caddy list-modules --versions)
DNS provider: Cloudflare (single API token, Zone:DNS:Edit + Zone:Read scoped)
Caddyfile relevant snippet:

caddyfile (cf_dns) {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
resolvers 1.1.1.1 1.0.0.1
}
}

example.sub.yourdomain.com {
import cf_dns
reverse_proxy 127.0.0.1:1234
}

Steps to reproduce

Configure a site with the Cloudflare DNS-01 challenge as above.
Trigger a certificate obtain (e.g. fresh container start for a domain with no existing cert).
While the attempt is in progress, query Cloudflare's API directly for the created record:

curl -s "https://api.cloudflare.com/client/v4/zones/<zone_id>/dns_records?type=TXT&name=_acme-challenge.example.sub.yourdomain.com"
-H "Authorization: Bearer "

Observe the content field includes literal " characters wrapping the value.

Evidence

For comparison, here are three TXT records that existed simultaneously at the same name (_acme-challenge..yourdomain.com) during debugging:

Sourcecontent field (raw, as returned by Cloudflare API)ttlManual curl -X POST (plain content, no quotes in payload)manual-test-value-12345120Caddy ACME attempt #1"OQJskOaeWRBc82svCMEYT99uN0gYhadxU5vscYMBibw"1Caddy ACME attempt #2"F3v_kA6KMzm88n5gooov9aH26bzKwhVu7q7SepNQQY0"1

The manually-created record (content submitted with no surrounding quotes) was stored and returned by Cloudflare exactly as submitted — no quotes added. Both Caddy-created records, from the same token and same zone, came back with literal quote characters baked into the content.

This was not a propagation/caching/resolver issue — ruled out by:

Setting explicit resolvers 1.1.1.1 1.0.0.1 in the tls block (no change in behavior)
Querying the zone's authoritative nameserver directly (jake.ns.cloudflare.com) during an active attempt — record never matched because of the quote mismatch, not because it was absent
Confirming via the Cloudflare API directly that the record existed in the zone the entire time, just with malformed content

Expected behavior

The TXT record content written to Cloudflare should be the raw challenge token, with no additional quote characters, matching what a manual API call with the same payload produces.

Actual behavior

The module (or an underlying dependency, possibly related to the recent libdns 1.0 API migration) appears to be double-wrapping the content value in quotes before submitting it to Cloudflare's API, resulting in a permanently broken DNS-01 challenge.

Regression window / workaround

This appears to be a regression introduced between the module version bundled with Caddy 2.11.3 and 2.11.4. Pinning to the 2.11.3-based image resolves the issue completely with an otherwise identical Caddyfile and Cloudflare token. I have not yet identified the exact caddy-dns/cloudflare (or libdns/cloudflare) version bundled with the 2.11.3 build for direct comparison — happy to pull that if useful for triage.

Additional context

Cloudflare's own API/dashboard does have a known general quirk around auto-wrapping TXT content in quotes in some circumstances (acknowledged in their own UI warning, and reported against several other tools: terraform-provider-cloudflare#4650, certbot#10048). That said, the fact that a manually-submitted unquoted payload was stored unquoted, while the module's own writes were quoted, suggests the quoting is happening on the request-construction side here rather than purely a Cloudflare-side inconsistency — but I'm not certain of the exact mechanism without digging into the module's source.

Assistance Disclosure

AI used

If AI was used, describe the extent to which it was used.

used Claude to debug the issue and write up parts of the issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions