A certificate covering both a domain and its wildcard, for instance
"example.com" and "*.example.com", gets two dns-01 authorizations from
the ACME server. Both have the same identifier value, "example.com"
(the wildcard one only has "wildcard": true), but each one has its own
token, so two different TXT records must be deployed under
"_acme-challenge.example.com".

HAProxy emits one "acme deploy" message per authorization, and an
external agent such as the Data Plane API answers each of them with
"acme challenge_ready <crt> domain example.com". However,
acme_challenge_ready() marks every pending challenge matching the domain
at once. The first call then marks both challenges as ready and triggers
the validation before the second TXT record is even deployed, which
makes the wildcard (or apex) validation fail. The second call finds no
task anymore and returns an error.

Only mark the first pending challenge matching the domain, so that each
call accounts for exactly one challenge. The number of remaining
challenges is still computed over all of them. Callers that signal
readiness once per deploy message, which is what the log message asks
for, get the expected behavior. The documentation of the "cli" value of
"challenge-ready" and of ACME.challenge_ready() is updated accordingly.

As a workaround, "challenge-ready cli,delay" with a large enough
"dns-delay" leaves time for the second record to be deployed.

This should be backported to 3.3. The code was moved into
acme_challenge_ready() in 3.5, so the fix has to be applied to
cli_acme_chall_ready_parse() in older versions.
---
 doc/configuration.txt |  6 +++++-
 doc/lua-api/index.rst |  2 ++
 src/acme.c            | 14 +++++++++-----
 3 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/doc/configuration.txt b/doc/configuration.txt
index 91e235f..68e14de 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -33121,7 +33121,11 @@ challenge-ready <value>[,<value>]*
            the stats socket. This allows an external DNS provisioning tool to
            confirm that the TXT record has been set before HAProxy proceeds.
            It is also possible to signal the "cli" readiness using the
-           ACME.challenge_ready() lua function.
+           ACME.challenge_ready() lua function. Each call marks a single
+           pending challenge as ready: a certificate covering both a domain
+           and its wildcard (e.g. "example.com" and "*.example.com") has two
+           challenges for the same <domain>, so the command must be issued
+           once for each of them.
 
     dns  - perform a DNS pre-check by resolving the TXT record for
            "_acme-challenge.<domain>" using the configured "default" resolvers
diff --git a/doc/lua-api/index.rst b/doc/lua-api/index.rst
index e46adc4..76512ec 100644
--- a/doc/lua-api/index.rst
+++ b/doc/lua-api/index.rst
@@ -4754,6 +4754,8 @@ ACME class
 .. js:function:: ACME.challenge_ready(crt, dns)
 
   Marks the ACME challenge for domain <dns> in certificate <crt> as ready.
+  Only one pending challenge is marked per call: a domain and its wildcard
+  share the same <dns> and must be signaled twice.
   Returns the number of remaining challenges, or 0 if all challenges are ready
   and validation has been triggered. Raises a Lua error if the certificate or
   domain is not found.
diff --git a/src/acme.c b/src/acme.c
index 63f6959..7df0ecf 100644
--- a/src/acme.c
+++ b/src/acme.c
@@ -3579,6 +3579,8 @@ static int cli_acme_renew_parse(char **args, char 
*payload, struct appctx *appct
 
 /*
  * Change the readiness of an ACME challenge per couple <crt>+<dns>
+ * Only one pending challenge is marked per call, since a domain and its
+ * wildcard share the same <dns> but have two distinct challenges.
  * Return:
  * - -2 if the crt was not found
  * - -1 if an non-ready couple crt+dns wasn't not found
@@ -3604,11 +3606,13 @@ int acme_challenge_ready(const char *crt, const char 
*dns)
        if (ctx->cfg->cond_ready & ACME_RDY_CLI)
                auth = ctx->auths;
        while (auth) {
-               if (isteq(ist(dns), auth->dns)) {
-                       if ((auth->ready & ACME_RDY_CLI) == 0) {
-                               auth->ready |= ACME_RDY_CLI;
-                               found++;
-                       }
+               /* Only mark one challenge per call: a domain and its wildcard
+                * have two different challenges sharing the same <dns>.
+                */
+               if (!found && isteq(ist(dns), auth->dns) &&
+                   (auth->ready & ACME_RDY_CLI) == 0) {
+                       auth->ready |= ACME_RDY_CLI;
+                       found++;
                }
                if ((auth->ready & ACME_RDY_CLI) == 0)
                        remain++;
-- 
2.43.0



Reply via email to