MINOR: acme: opportunistic DNS check for dns-persist-01 to skip challenge-ready steps

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.
This commit is contained in:
William Lallemand 2026-04-15 15:46:26 +02:00
parent 5fe0579d49
commit c295a5c861
3 changed files with 119 additions and 23 deletions

View 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

View File

@ -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,

View 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;