MEDIUM: acme: split the initial delay from the retry DNS delay

The previous ACME_RSLV_WAIT state served a dual role: it applied the
initial dns-delay before the first DNS probe and also handled the
delay between retries. There was no way to simply wait a fixed delay
before submitting the challenge without also triggering DNS pre-checks.

Replace ACME_RSLV_WAIT with two distinct states:
  - ACME_INITIAL_DELAY: an optional initial wait before proceeding,
    only applied when "challenge-ready" includes the new "delay" keyword
  - ACME_RSLV_RETRY_DELAY: the delay between resolution retries, always
    applied when DNS pre-checks are in progress

The new "delay" keyword in "challenge-ready" can be used standalone
(wait then submit the challenge directly) or combined with "dns" (wait
then start the DNS pre-checks). When "delay" is not set, the first DNS
probe fires immediately.

Update the documentation accordingly.
This commit is contained in:
William Lallemand 2026-04-02 16:15:12 +02:00
parent 6ca83eb731
commit 6df3662077
3 changed files with 88 additions and 28 deletions

View File

@ -32290,14 +32290,19 @@ challenge-ready <value>[,<value>]*
option is independent of the CLI command, so no human intervention
is required.
delay - apply an initial wait of "dns-delay" before proceeding. Without
"dns", the challenge is submitted after the delay expires. When
combined with "dns", the initial wait is applied before starting
the DNS pre-checks.
none - no readiness condition; the challenge is submitted to the ACME
server immediately without waiting for any external confirmation.
This option cannot be combined with others.
Multiple values can be combined with a comma so that both conditions must be
met. The order of the values is not significant. When "cli" and "dns" are
combined, HAProxy first waits for the CLI confirmation before triggering the
DNS propagation check.
Multiple values can be combined with a comma. When several conditions are
specified, HAProxy processes them in the following order: first it waits for
the CLI confirmation ("cli"), then applies the initial delay ("delay"), then
performs the DNS pre-checks ("dns").
This option is only compatible with the dns-01 challenge type.
@ -32322,9 +32327,20 @@ directory <string>
directory https://acme-staging-v02.api.letsencrypt.org/directory
dns-delay <time>
When "challenge-ready" includes "dns", configure the delay before the first
DNS resolution attempt and between retries. The value is a time expressed in
HAProxy time format (e.g. "5m", "300s"). Default is 30 seconds.
Configure the delay used by "challenge-ready" conditions "delay" and "dns".
The value is a time expressed in HAProxy time format (e.g. "5m", "300s").
Default is 30 seconds.
Its role depends on the "challenge-ready" conditions in use:
delay - the challenge is submitted after this delay expires, without
any DNS pre-check.
dns - the delay between two consecutive DNS resolution attempts.
The first probe fires immediately without any initial wait.
dns+delay - the initial wait before the first DNS resolution attempt, and
the delay between subsequent retries.
Note that the resolution goes through the configured "default" resolvers
section, not the authoritative name servers. Results may therefore still be

View File

@ -14,6 +14,7 @@
#define ACME_RDY_NONE 0x00
#define ACME_RDY_CLI 0x01
#define ACME_RDY_DNS 0x02
#define ACME_RDY_DELAY 0x04
/* acme section configuration */
struct acme_cfg {
@ -52,7 +53,8 @@ enum acme_st {
ACME_NEWORDER,
ACME_AUTH,
ACME_CLI_WAIT, /* wait for the ACME_RDY_CLI */
ACME_RSLV_WAIT,
ACME_INITIAL_DELAY,
ACME_RSLV_RETRY_DELAY,
ACME_RSLV_TRIGGER,
ACME_RSLV_READY,
ACME_CHALLENGE,

View File

@ -115,22 +115,23 @@ 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_RSLV_WAIT: chunk_appendf(&trace_buf, "ACME_RSLV_WAIT"); 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_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)) {
@ -480,6 +481,9 @@ static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx
} else if (strcmp(str, "dns") == 0) {
/* wait for the DNS-check to run the challenge */
cur_acme->cond_ready |= ACME_RDY_DNS;
} else if (strcmp(str, "delay") == 0) {
/* wait for the DNS-check to run the challenge */
cur_acme->cond_ready |= ACME_RDY_DELAY;
} else if (strcmp(str, "none") == 0) {
if (cur_acme->cond_ready || (saveptr && *saveptr)) {
err_code |= ERR_ALERT | ERR_FATAL;
@ -2417,11 +2421,49 @@ re:
goto wait;
/* next step */
st = ACME_RSLV_WAIT;
st = ACME_INITIAL_DELAY;
goto nextreq;
}
break;
case ACME_RSLV_WAIT: {
case ACME_INITIAL_DELAY: {
struct acme_auth *auth;
int all_cond_ready = ctx->cfg->cond_ready;
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;
}
/* if we don't have an initial delay, let's trigger */
if (!(ctx->cfg->cond_ready & ACME_RDY_DELAY)) {
st = ACME_RSLV_TRIGGER;
goto nextreq;
}
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
auth->ready |= ACME_RDY_DELAY;
}
/* either trigger the resolution of the challenge */
if (ctx->cfg->cond_ready & ACME_RDY_DNS)
st = ACME_RSLV_TRIGGER;
else
st = ACME_CHALLENGE;
ctx->http_state = ACME_HTTP_REQ;
ctx->state = st;
send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: waiting %ds\n",
ctx->store->path, ctx->cfg->dns_delay);
task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
return task;
}
break;
case ACME_RSLV_RETRY_DELAY: {
struct acme_auth *auth;
int all_cond_ready = ctx->cfg->cond_ready;
@ -2448,7 +2490,7 @@ re:
st = ACME_RSLV_TRIGGER;
ctx->http_state = ACME_HTTP_REQ;
ctx->state = st;
send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: triggering the resolution in %ds\n",
send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: retrying the resolution in %ds\n",
ctx->store->path, ctx->cfg->dns_delay);
task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
@ -2519,7 +2561,7 @@ re:
}
/* not all ready yet, retry after dns-delay */
st = ACME_RSLV_WAIT;
st = ACME_RSLV_RETRY_DELAY;
ctx->http_state = ACME_HTTP_REQ;
ctx->state = st;
goto nextreq;