]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: acme: opportunistic DNS check for dns-persist-01 to skip challenge-ready steps
authorWilliam Lallemand <wlallemand@haproxy.com>
Wed, 15 Apr 2026 13:46:26 +0000 (15:46 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Wed, 15 Apr 2026 13:57:57 +0000 (15:57 +0200)
For dns-persist-01, the "_validation-persist.<domain>" TXT record is set once
and never changes between renewals. Add an initial opportunistic DNS check
(ACME_INITIAL_RSLV_TRIGGER / ACME_INITIAL_RSLV_READY states) that runs before
the challenge-ready conditions are evaluated. If all domains already have the
TXT record, the challenge is submitted immediately without going through the
cli/delay/dns challenge-ready steps, making renewals faster once the record is
in place.

The new ACME_RDY_INITIAL_DNS flag is automatically set for
dns-persist-01 in cond_ready.

doc/configuration.txt
include/haproxy/acme-t.h
src/acme.c

index 1568978fb1cf728de9a5a3a0b221f6217c2b9b0c..16d40710cebb179a4e6a5a882310e55620ea9f85 100644 (file)
@@ -32461,6 +32461,15 @@ challenge-ready <value>[,<value>]*
   When "challenge" is set to "dns-persist-01" and this option is not
   configured, the default is "dns,delay".
 
+  When "challenge" is set to "dns-persist-01", an initial opportunistic DNS
+  check is always performed before the challenge-ready conditions are evaluated.
+  Since the "_validation-persist.<domain>" TXT record is set once and never
+  changes between renewals, HAProxy checks at renewal time whether the record
+  is already present. If the check succeeds for all domains, the challenge is
+  submitted immediately without going through the challenge-ready steps (cli,
+  delay, dns). If the check fails, HAProxy falls back to the normal
+  challenge-ready flow.
+
   Example:
     # Wait for CLI confirmation, then verify DNS propagation
     challenge-ready cli,dns
index 4418d369be7d1b5701bb639852f006934acf9caf..b12a95be1042d4a9f21f54038229fdc4dab04519 100644 (file)
 #define ACME_RETRY 5
 
 /* Readiness requirements for challenge */
-#define ACME_RDY_NONE  0x00
-#define ACME_RDY_CLI   0x01
-#define ACME_RDY_DNS   0x02
-#define ACME_RDY_DELAY 0x04
+#define ACME_RDY_NONE         0x00
+#define ACME_RDY_CLI          0x01
+#define ACME_RDY_DNS          0x02
+#define ACME_RDY_DELAY        0x04
+#define ACME_RDY_INITIAL_DNS  0x08
 
 /* acme section configuration */
 struct acme_cfg {
@@ -53,6 +54,8 @@ enum acme_st {
        ACME_NEWORDER,
        ACME_AUTH,
        ACME_CLI_WAIT,               /* wait for the ACME_RDY_CLI */
+       ACME_INITIAL_RSLV_TRIGGER,   /* opportunistic DNS check avoid cond_ready steps */
+       ACME_INITIAL_RSLV_READY,
        ACME_INITIAL_DELAY,
        ACME_RSLV_RETRY_DELAY,
        ACME_RSLV_TRIGGER,
index 0756cd2493fb8b7d5e3f6a2cefacc0e24bc0b974..7a392d6d20dd3af3ea2444933681d0b68ec156bd 100644 (file)
@@ -117,23 +117,25 @@ static void acme_trace(enum trace_level level, uint64_t mask, const struct trace
                }
                chunk_appendf(&trace_buf, ", st: ");
                switch (ctx->state) {
-                       case ACME_RESOURCES:         chunk_appendf(&trace_buf, "ACME_RESOURCES");        break;
-                       case ACME_NEWNONCE:          chunk_appendf(&trace_buf, "ACME_NEWNONCE");         break;
-                       case ACME_CHKACCOUNT:        chunk_appendf(&trace_buf, "ACME_CHKACCOUNT");       break;
-                       case ACME_NEWACCOUNT:        chunk_appendf(&trace_buf, "ACME_NEWACCOUNT");       break;
-                       case ACME_NEWORDER:          chunk_appendf(&trace_buf, "ACME_NEWORDER");         break;
-                       case ACME_AUTH:              chunk_appendf(&trace_buf, "ACME_AUTH");             break;
-                       case ACME_CLI_WAIT :         chunk_appendf(&trace_buf, "ACME_CLI_WAIT");         break;
-                       case ACME_INITIAL_DELAY:     chunk_appendf(&trace_buf, "ACME_INITIAL_DELAY");    break;
-                       case ACME_RSLV_RETRY_DELAY:  chunk_appendf(&trace_buf, "ACME_RSLV_RETRY_DELAY"); break;
-                       case ACME_RSLV_TRIGGER:      chunk_appendf(&trace_buf, "ACME_RSLV_TRIGGER");     break;
-                       case ACME_RSLV_READY:        chunk_appendf(&trace_buf, "ACME_RSLV_READY");       break;
-                       case ACME_CHALLENGE:         chunk_appendf(&trace_buf, "ACME_CHALLENGE");        break;
-                       case ACME_CHKCHALLENGE:      chunk_appendf(&trace_buf, "ACME_CHKCHALLENGE");     break;
-                       case ACME_FINALIZE:          chunk_appendf(&trace_buf, "ACME_FINALIZE");         break;
-                       case ACME_CHKORDER:          chunk_appendf(&trace_buf, "ACME_CHKORDER");         break;
-                       case ACME_CERTIFICATE:       chunk_appendf(&trace_buf, "ACME_CERTIFICATE");      break;
-                       case ACME_END:               chunk_appendf(&trace_buf, "ACME_END");              break;
+                       case ACME_RESOURCES:                chunk_appendf(&trace_buf, "ACME_RESOURCES");               break;
+                       case ACME_NEWNONCE:                 chunk_appendf(&trace_buf, "ACME_NEWNONCE");                break;
+                       case ACME_CHKACCOUNT:               chunk_appendf(&trace_buf, "ACME_CHKACCOUNT");              break;
+                       case ACME_NEWACCOUNT:               chunk_appendf(&trace_buf, "ACME_NEWACCOUNT");              break;
+                       case ACME_NEWORDER:                 chunk_appendf(&trace_buf, "ACME_NEWORDER");                break;
+                       case ACME_AUTH:                     chunk_appendf(&trace_buf, "ACME_AUTH");                    break;
+                       case ACME_CLI_WAIT :                chunk_appendf(&trace_buf, "ACME_CLI_WAIT");                break;
+                       case ACME_INITIAL_RSLV_TRIGGER:     chunk_appendf(&trace_buf, "ACME_INITIAL_RSLV_TRIGGER");    break;
+                       case ACME_INITIAL_RSLV_READY:       chunk_appendf(&trace_buf, "ACME_INITIAL_RSLV_READY");      break;
+                       case ACME_INITIAL_DELAY:            chunk_appendf(&trace_buf, "ACME_INITIAL_DELAY");           break;
+                       case ACME_RSLV_RETRY_DELAY:         chunk_appendf(&trace_buf, "ACME_RSLV_RETRY_DELAY");        break;
+                       case ACME_RSLV_TRIGGER:             chunk_appendf(&trace_buf, "ACME_RSLV_TRIGGER");            break;
+                       case ACME_RSLV_READY:               chunk_appendf(&trace_buf, "ACME_RSLV_READY");              break;
+                       case ACME_CHALLENGE:                chunk_appendf(&trace_buf, "ACME_CHALLENGE");               break;
+                       case ACME_CHKCHALLENGE:             chunk_appendf(&trace_buf, "ACME_CHKCHALLENGE");            break;
+                       case ACME_FINALIZE:                 chunk_appendf(&trace_buf, "ACME_FINALIZE");                break;
+                       case ACME_CHKORDER:                 chunk_appendf(&trace_buf, "ACME_CHKORDER");                break;
+                       case ACME_CERTIFICATE:              chunk_appendf(&trace_buf, "ACME_CERTIFICATE");             break;
+                       case ACME_END:                      chunk_appendf(&trace_buf, "ACME_END");                     break;
                }
        }
        if (mask & (ACME_EV_REQ|ACME_EV_RES)) {
@@ -769,6 +771,10 @@ static int cfg_postsection_acme()
        char store_path[PATH_MAX]; /* complete path with crt_base */
        struct stat st;
 
+       /* if dns-persist-01 is set, add an extra INITIAL_DNS check */
+       if (strcasecmp(cur_acme->challenge, "dns-persist-01") == 0)
+               cur_acme->cond_ready |= ACME_RDY_INITIAL_DNS;
+
        /* if account key filename is unspecified, choose a filename for it */
        if (!cur_acme->account.file) {
                if (!memprintf(&cur_acme->account.file, "%s.account.key", cur_acme->name)) {
@@ -2459,8 +2465,9 @@ re:
                                }
                                if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
                                        if ((strcasecmp(ctx->cfg->challenge, "dns-01") == 0 ||
-                                    strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) && ctx->cfg->cond_ready)
-                                               st = ACME_CLI_WAIT;
+                                            strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) &&
+                                           ctx->cfg->cond_ready)
+                                               st = ACME_INITIAL_RSLV_TRIGGER;
                                        else
                                                st = ACME_CHALLENGE;
                                        ctx->next_auth = ctx->auths;
@@ -2469,6 +2476,83 @@ re:
                                goto nextreq;
                        }
                break;
+               case ACME_INITIAL_RSLV_TRIGGER: {
+                       /* trigger an initial dns propagation check that will
+                        * remove the challenge-ready requirements if valid */
+                       struct acme_auth *auth;
+                       int all_cond_ready = ctx->cfg->cond_ready;
+
+                       /* if we don't have an initial dns propagation check, let's go to the next cond_ready */
+                       if (!(ctx->cfg->cond_ready & ACME_RDY_INITIAL_DNS)) {
+                               st = ACME_CLI_WAIT;
+                               goto nextreq;
+                       }
+
+                       for (auth = ctx->auths; auth != NULL; auth = auth->next) {
+                               all_cond_ready &= auth->ready;
+                       }
+
+                       /* if everything is ready, let's do the challenge request */
+                       if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
+                               st = ACME_CHALLENGE;
+                               goto nextreq;
+                       }
+
+                       for (auth = ctx->auths; auth != NULL; auth = auth->next) {
+                               if (auth->ready == ctx->cfg->cond_ready)
+                                       continue;
+
+                               HA_ATOMIC_INC(&ctx->dnstasks);
+
+                               auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, ctx->cfg->challenge, &errmsg);
+                               if (!auth->rslv)
+                                       goto abort;
+                               auth->rslv->acme_task = task;
+                       }
+                       st = ACME_INITIAL_RSLV_READY;
+                       goto wait;
+               }
+               break;
+               case ACME_INITIAL_RSLV_READY: {
+                       struct acme_auth *auth;
+                       int all_ready = 1;
+
+                       /* if triggered by the CLI, wait for the DNS tasks to
+                        * finish
+                        */
+                        if (HA_ATOMIC_LOAD(&ctx->dnstasks) != 0)
+                               goto wait;
+
+                       /* triggered by the latest DNS task */
+                       for (auth = ctx->auths; auth != NULL; auth = auth->next) {
+                               if (auth->ready == ctx->cfg->cond_ready)
+                                       continue;
+                               if (auth->rslv->result == RSLV_STATUS_VALID) {
+                                       if (strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) {
+                                               auth->ready |= ACME_RDY_INITIAL_DNS;
+                                       }
+                               } else {
+                                       all_ready = 0;
+                               }
+
+                               acme_rslv_free(auth->rslv);
+                               auth->rslv = NULL;
+                       }
+                       if (all_ready) {
+                               /* opportunistic validation, don't do the
+                                * cond_ready steps */
+                               st = ACME_CHALLENGE;
+                               ctx->cfg->cond_ready &= ACME_RDY_INITIAL_DNS;
+                               ctx->next_auth = ctx->auths;
+                               goto nextreq;
+                       }
+
+                       /* opportunistic DNS check failed, try the ready_cond */
+                       st = ACME_RSLV_RETRY_DELAY;
+                       goto nextreq;
+               }
+               break;
+
                case ACME_CLI_WAIT: {
                        struct acme_auth *auth;
                        int all_cond_ready = ctx->cfg->cond_ready;