From c295a5c86155238b8c210c91579a2acaff9381b5 Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Wed, 15 Apr 2026 15:46:26 +0200 Subject: [PATCH] MINOR: acme: opportunistic DNS check for dns-persist-01 to skip challenge-ready steps For dns-persist-01, the "_validation-persist." 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 | 9 +++ include/haproxy/acme-t.h | 11 ++-- src/acme.c | 122 +++++++++++++++++++++++++++++++++------ 3 files changed, 119 insertions(+), 23 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 1568978fb..16d40710c 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -32461,6 +32461,15 @@ challenge-ready [,]* 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." 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 diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h index 4418d369b..b12a95be1 100644 --- a/include/haproxy/acme-t.h +++ b/include/haproxy/acme-t.h @@ -11,10 +11,11 @@ #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, diff --git a/src/acme.c b/src/acme.c index 0756cd249..7a392d6d2 100644 --- a/src/acme.c +++ b/src/acme.c @@ -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;