Error fetching cert with cloudflare

1. The problem I’m having:

I’m using a cloudflare api token (I’ve tried both user and account api keys) and caddy is unable to obtain a certificate.

2. Error messages and/or full log output:


{"level":"info","ts":1759808468.1592867,"msg":"trying to solve challenge","identifier":"squarephone.biz","challenge_type":"dns-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"info","ts":1759808468.3930035,"msg":"trying to solve challenge","identifier":"video.squarephone.biz","challenge_type":"dns-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1759808469.245388,"msg":"cleaning up solver","identifier":"squarephone.biz","challenge_type":"dns-01","error":"no memory of presenting a DNS record for \"_acme-challenge.squarephone.biz\" (usually OK if presenting also failed)","stacktrace":"github.com/mholt/acmez/v3.(*Client).solveChallenges.func1\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:318\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:363\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1759808469.4680657,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"squarephone.biz","issuer":"acme-staging-v02.api.letsencrypt.org-directory","error":"[squarephone.biz] solving challenges: presenting for challenge: adding temporary record for zone \"biz.\": expected 1 zone, got 0 for biz. (order=https://acme-staging-v02.api.letsencrypt.org/acme/order/233051223/27712949623) (ca=https://acme-staging-v02.api.letsencrypt.org/directory)"}
{"level":"error","ts":1759808469.4681072,"logger":"tls.obtain","msg":"will retry","error":"[squarephone.biz] Obtain: [squarephone.biz] solving challenges: presenting for challenge: adding temporary record for zone \"biz.\": expected 1 zone, got 0 for biz. (order=https://acme-staging-v02.api.letsencrypt.org/acme/order/233051223/27712949623) (ca=https://acme-staging-v02.api.letsencrypt.org/directory)","attempt":5,"retrying_in":600,"elapsed":608.643667406,"max_duration":2592000}

3. Caddy version:

docker exec -it caddy caddy --versionv2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

Its running in a docker container, using docker-compose.
I’ve confirmed that the token is correctly named with the correct key in the container.

From within the container you can see the following env:

SHLVL=1
HOME=/root
OLDPWD=/opt/xxxx/caddy/sites
CADDY_VERSION=v2.10.0
ACME_AGREE=true
ACTIVE_SITE=sites/dev/*.caddy
TERM=xterm
ACME_URL=https://acme-staging-v02.api.letsencrypt.org/directory
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
XDG_CONFIG_HOME=/opt/xxxx/caddy/config
XDG_DATA_HOME=/opt/xxxx/caddy/data
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXX
PWD=/opt/xxxx/caddy

a. System environment:

docker see the above docker file.

b. Command:

The caddy command is run by the above docker container.

c. Service/unit/compose file:

caddy:
    container_name: caddy
    image: xxx/xxx-caddy:${XXX_VERSION}
    restart: always
    network_mode: "host"
    cap_add:
      - NET_ADMIN
    environment:
      ACME_AGREE: "true"
      ACME_URL: ${ACME_URL} # staging or production
      EMAIL: ${AUTH_PROVIDER_EMAIL_ADDRESS}
      CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
      ACTIVE_SITE: ${ACTIVE_SITE}

    ports:
      - 80:80
      - 443:443
      - 443:443/udp

    volumes:
      # Persist certificates
      - caddy:/caddy
      - filestore:/opt/xxx/filestore
      #- /opt/xxx/caddy:/opt/xxx/caddy

      # Logs
      - /tmp/caddy:/var/log/caddy

    logging:
      driver: "journald"

# we build our own caddy file as we need the cloudflare module.
FROM caddy:2.10.0-builder AS builder


# https://caddyserver.com/docs/modules/dns.providers.cloudflare
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/WeidiDeng/caddy-cloudflare-ip \
    --with github.com/corazawaf/coraza-caddy/v2


FROM caddy:2.10.0

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

# Set custom directories for data and config
ENV XDG_CONFIG_HOME=/opt/xxxx/caddy/config
ENV XDG_DATA_HOME=/opt/xxx/caddy/data

# RUN mkdir -p /caddy
# coraza WAF configuation directory
RUN mkdir -p /opt/xxxx/caddy/coraza

COPY config/caddy/ /opt/xxxx/caddy/
# waf configuration files.
COPY config/coraza /opt/xxxx/caddy/coraza
# so we know where to put any custom waf commands.
RUN touch /opt/xxxx/caddy/coraza/waf-directives.conf

CMD ["caddy", "run", "--config", "/opt/xxx/caddy/Caddyfile", "--adapter", "caddyfile"]

d. My complete Caddy config:

The caddy config is split over multiple files:

Caddyfile

# Dev: squarephone.biz (same app/WAF/heartbeat as onepub.dev)
squarephone.biz {
        # Typically keep HSTS off for dev to avoid stickiness; remove '-' to enable.
        header {
                -Strict-Transport-Security
        }

        import cloudflare-tls
        import common-security
        import common-app-routes
        import common-mailhog
}
/opt/XXXX/caddy/sites/dev # cd ..
/opt/XXXX/caddy/sites # cd ..
/opt/XXXX/caddy # cat Caddyfile 
{
	# ===== Global =====
	email {$EMAIL}
	acme_ca {$ACME_URL:"https://acme-staging-v02.api.letsencrypt.org/directory"}

	servers {
		trusted_proxies cloudflare {
			interval 12h
			timeout 15s
		}
	}

	order coraza_waf first
}

import common.caddy

# Only these will be loaded; set ACTIVE_IMPORTS to a space-separated list of site files.
# e.g.
#   ACTIVE_SITE="sites/production/*.caddy"
#   ACTIVE_SITE="sites/beta/*.caddy"
#   ACTIVE_SITE="sites/dev/*.caddy"
import "{$ACTIVE_SITE}"

common.caddy



# ===== Reusable TLS (single CF token for all zones) =====
(cloudflare-tls) {
        tls {
                dns cloudflare {
                        api_token {$CLOUDFLARE_API_TOKEN}
                }
        }
}

# ===== Reusable snippets =====
(common-security) {
        header {
                Strict-Transport-Security "max-age=31536000; includeSubDomains"
                X-Frame-Options "SAMEORIGIN"
                X-Content-Type-Options "nosniff"
        }
        @block_ai header_regexp User-Agent "(?i)(GPTBot|ChatGPT|Google-Extended|Claude-Web|Anthropic|Amazonbot|FacebookBot|cohere-ai|Bytespider|YouBot)"
        respond @block_ai 403
        @block_php path *.php
        abort @block_php
        @multiple_slashes path_regexp multipleSlashes ^(.*)//+(.*)$
        redir @multiple_slashes {scheme}://{host}{re.multipleSlashes.1}/{re.multipleSlashes.2}{query} permanent
        encode gzip
}

(common-waf) {
        coraza_waf {
                load_owasp_crs
                directives `
                Include @coraza.conf-recommended
                Include @crs-setup.conf.example
                Include @owasp_crs/*.conf
                Include /opt/xxxx/caddy/coraza/waf-directives.conf
                `
        }
}

(common-proxy8080) {
        reverse_proxy 127.0.0.1:8080 {
                header_up Host {host}
                header_up X-Real-IP {remote_host}
                transport http {
                        read_timeout 300s
                }
                header_up Connection {header.Connection}
                header_up Upgrade {header.Upgrade}
        }
}

(common-app-routes) {

        # we need to bypass WAF for some of our internal
        # end points that look a little odd to WAF.

        # WebSocket upgrades (keep first)
        @ws {
                header_regexp Upgrade (?i)websocket
                header_regexp Connection (?i)\bupgrade\b
        }
        handle @ws { 
            import common-proxy8080 
        }

        # UIDL calls like "/?v-r=uidl&v-uiId=..."
        @vaadin_uidl {
                path /
                query v-r=uidl
                query v-uiId=*
        }
        handle @vaadin_uidl { 
                import common-proxy8080 
        }

        # Vaadin Push long-poll/stream: "/VAADIN/push?v-r=push&v-uiId=...&v-pushId=..."
        @vaadin_push {
                path /VAADIN/push
                query v-r=push
                query v-uiId=*
                query v-pushId=*
        }
        handle @vaadin_push { 
                import common-proxy8080 
        }

        # Your endpoint that currently sends text/plain
        @visibility path /api/visibility
        handle @visibility { 
                import common-proxy8080 
        }

        # Heartbeat (already bypassed, keep consistent)
        @heartbeat path /api/clientheartbeat
        handle @heartbeat { 
                import common-proxy8080 
        }

        # Everything else under WAF
        handle {
                import common-waf
                import common-proxy8080
        }

        # API error mapping
        handle_errors {
                @api path /api/*
                handle @api {
                        root * /etc/caddy/json
                        rewrite 502 /500.json
                        rewrite 404 /404.json
                        file_server
                }
        }

        # Default upstream
        import common-proxy8080
}

(common-mailhog) {
        handle /mailhog/* {
                basic_auth {
                        {$EMAIL} XXXXXXXXXXXXXXXXXXXXX
                }
                reverse_proxy 127.0.0.1:8025
        }
}


(common-video) {
  # TLS via Cloudflare DNS for video hosts
  import cloudflare-tls

  # Security headers, gzip, etc.
  import common-security

  # Apply WAF by default (your app is serving the video files)
  import common-waf

  # MP4 proxy (shared)
  @mp4 path *.mp4
  handle @mp4 {
    reverse_proxy 127.0.0.1:8080 {
      header_up Host {host}
      header_up X-Real-IP {remote_host}
      transport http {
        read_timeout 300s
      }
      header_up Connection {header.Connection}
      header_up Upgrade {header.Upgrade}
    }
  }
}

The file referenced via ACTIVE_SITE

# Dev: squarephone.biz (same app/WAF/heartbeat as onepub.dev)
squarephone.biz {
        # Typically keep HSTS off for dev to avoid stickiness; remove '-' to enable.
        header {
                -Strict-Transport-Security
        }

        import cloudflare-tls
        import common-security
        import common-app-routes
        import common-mailhog
}

5. Links to relevant resources:

I’ve tried the following which works:

```
url -H “Authorization: Bearer XXXXXXXXXXXXXXXXXXX” “https://api.cloudflare.com/client/v4/zones” --get --data-urlencode “name=squarephone.biz” | jq “{ success, errors, results: [.result? | { id, name }]}”
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1208 0 1208 0 0 2846 0 --:–:-- --:–:-- --:–:-- 2849
{
“success”: true,
“errors”: ,
“results”: [
{
“id”: “b6bca6bfa0065ccbed629d386bcc2490”,
“name”: “squarephone.biz”
}
]
}
``

```
curl -H “Authorization: Bearer XXXXXXXXX” “https://api.cloudflare.com/client/v4/zones/b6bca6bfa0065ccbed629d386bcc2490/dns_records” -X POST --data ‘{“type”:“TXT”,“name”:“_acme-challenge.files”,“content”:“dummy_value”}’ | jq “{ success, errors, result: (.result | { zone_name, name, type, content }) }”
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 424 0 355 100 69 350 68 0:00:01 0:00:01 --:–:-- 418
{
“success”: true,
“errors”: ,
“result”: {
“zone_name”: null,
“name”: “_acme-challenge.files.squarephone.biz”,
“type”: “TXT”,
“content”: “dummy_value”
}
}
```

I’ve also confirmed that the docker container sees the same api token as the above curl tests use.

I’ve installed curl into the container and have run the following tests.

```
curl -H “Authorization: Bearer XXXXXX” “https://api.cloudflare.com/client/v4/zones” --get --data-urlencode “name=squa
rephone.biz”
{“result”:[{“id”:“b6bca6bfa0065ccbed629d386bcc2490”,“name”:“squarephone.biz”,“status”:“active”,“paused”:false,“type”:“full”,“development_mode”:0,“name_servers”:[“rene.ns.cloudflare.com”,“stella.ns.cloudflare.com”],“original_name_servers”:null,“original_registrar”:null,“original_dnshost”:null,“modified_on”:“2025-04-12T03:54:11.426802Z”,“created_on”:“2022-10-26T02:36:21.661132Z”,“activated_on”:“2022-10-26T02:52:32.542845Z”,“vanity_name_servers”:,“vanity_name_servers_ips”:null,“meta”:{“step”:2,“custom_certificate_quota”:0,“page_rule_quota”:3,“phishing_detected”:false},“owner”:{“id”:null,“type”:“user”,“email”:null},“account”:{“id”:“16824cb12c0a5c2a6c8d209e11dd6e98”,“name”:“Dev@onepub.dev’s Account”},“tenant”:{“id”:null,“name”:null},“tenant_unit”:{“id”:null},“permissions”:[“#zone:read”,“#zone_settings:read”,“#dns_records:edit”,“#dns_records:read”],“plan”:{“id”:“0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee”,“name”:“Free Website”,“price”:0,“currency”:“USD”,“frequency”:“”,“is_subscribed”:false,“can_subscribe”:false,“legacy_id”:“free”,“legacy_discount”:false,“externally_managed”:false}}],“result_info”:{“page”:1,“per_page”:20,“total_pages”:1,“count”:1,“total_count”:1},“success”:true,“errors”:,“messages”:}
```
and creating a record works.

```
curl -H “Authorization: Bearer XXXX” “https://api.cloudflare.com/client/v4/zones/b6bca6bfa0065ccbed629d386bcc
2490/dns_records” -X POST --data ‘{“type”:“TXT”,“name”:“_acme-challenge.files”,“content”:“dummy_value”}’
{“result”:{“id”:“e4eeb12065ddf02f63282308252d5fa2”,“name”:“_acme-challenge.files.squarephone.biz”,“type”:“TXT”,“content”:“dummy_value”,“proxiable”:false,“proxied”:false,“ttl”:1,“settings”:{},“meta”:{},“comment”:null,“tags”:,“created_on”:“2025-10-07T04:32:57.320777Z”,“modified_on”:“2025-10-07T04:32:57.320777Z”},“success”:true,“errors”:,“messages”:}

```

so I just rolled the keys and now it works. Aaaaaarg.

I have a sneaking suspicion that cloudflare key managed is a bit flaky because I think I’ve had a similar issue with cloudflare keys previously (not working after editing a key).

Update:

There was also a second issue in that if I had a local dns entry (/etc/hosts) which pointed to a private ip address this would also stop caddy being able to get a cert.

So now I’m not entirely sure it was rolling the keys that resolved the issue.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.