MINOR: acme: add 'dns-timeout' keyword for dns-01 challenge

When using the dns-01 challenge method with "challenge-ready dns", HAProxy
retries DNS resolution indefinitely at the interval set by "dns-delay". This
adds a "dns-timeout" keyword to set a maximum duration for the DNS check phase
(default: 600s). If the next resolution attempt would be scheduled beyond that
deadline, the renewal is aborted with an explicit error message.

A new "dnsstarttime" field is stored in the acme_ctx to record when DNS
resolution began, used to evaluate the timeout on each retry.
This commit is contained in:
William Lallemand 2026-04-01 18:30:06 +02:00
parent c49facbabe
commit 7f6999b764
3 changed files with 54 additions and 0 deletions

View File

@ -32320,6 +32320,18 @@ dns-delay <time>
section, not the authoritative name servers. Results may therefore still be
affected by DNS caching at the resolver level.
dns-timeout <time>
When "challenge-ready" includes "dns", configure the maximum time allowed to
successfully resolve the TXT record before aborting the challenge. The value
is a time expressed in HAProxy time format (e.g. "10m", "600s"). Default is
600 seconds.
If the next DNS resolution attempt would be triggered after the timeout has
elapsed (taking into account "dns-delay"), the challenge is aborted with an
error. This prevents an infinite retry loop when DNS propagation fails.
See also: "dns-delay"
keytype <string>
Configure the type of key that will be generated. Value can be either "RSA"
or "ECDSA". You can also configure the "curves" for ECDSA and the number of

View File

@ -23,6 +23,7 @@ struct acme_cfg {
int reuse_key; /* do we need to renew the private key */
int cond_ready; /* ready condition */
unsigned int dns_delay; /* delay in seconds before re-triggering DNS resolution (default: 300) */
unsigned int dns_timeout; /* time after which the DNS check shouldn't be retried (default: 600) */
char *directory; /* directory URL */
char *map; /* storage for tokens + thumbprint */
struct {
@ -100,6 +101,7 @@ struct acme_ctx {
struct ist finalize;
struct ist certificate;
unsigned int dnstasks; /* number of DNS tasks running for this ctx */
unsigned int dnsstarttime; /* time at which we started the DNS checks */
struct task *task;
struct ebmb_node node;
char name[VAR_ARRAY];

View File

@ -198,6 +198,7 @@ struct acme_cfg *new_acme_cfg(const char *name)
ret->challenge = strdup("http-01"); /* default value */
ret->dns_delay = 300; /* default DNS re-trigger delay in seconds */
ret->dns_timeout = 600; /* default DNS retry timeout */
/* The default generated keys are EC-384 */
ret->key.type = EVP_PKEY_EC;
@ -524,6 +525,31 @@ static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
} else if (strcmp(args[0], "dns-timeout") == 0) {
const char *res;
if (!*args[1]) {
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
if (alertif_too_many_args(1, file, linenum, args, &err_code))
goto out;
res = parse_time_err(args[1], &cur_acme->dns_timeout, TIME_UNIT_S);
if (res == PARSE_TIME_OVER) {
ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
} else if (res == PARSE_TIME_UNDER) {
ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
} else if (res) {
ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to '%s'\n", file, linenum, *res, args[0]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
} else if (strcmp(args[0], "reuse-key") == 0) {
if (!*args[1]) {
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
@ -930,6 +956,7 @@ static struct cfg_kw_list cfg_kws_acme = {ILH, {
{ CFG_ACME, "reuse-key", cfg_parse_acme_kws },
{ CFG_ACME, "challenge-ready", cfg_parse_acme_kws },
{ CFG_ACME, "dns-delay", cfg_parse_acme_kws },
{ CFG_ACME, "dns-timeout", cfg_parse_acme_kws },
{ CFG_ACME, "acme-vars", cfg_parse_acme_vars_provider },
{ CFG_ACME, "provider-name", cfg_parse_acme_vars_provider },
{ CFG_GLOBAL, "acme.scheduler", cfg_parse_global_acme_sched },
@ -2388,6 +2415,19 @@ re:
if ((ctx->cfg->cond_ready & ACME_RDY_CLI) && !(all_cond_ready & ACME_RDY_CLI))
goto wait;
/* set the start time of the DNS checks so we can apply
* the timeout */
if (ctx->dnsstarttime == 0)
ctx->dnsstarttime = ns_to_sec(now_ns);
/* Check if the next resolution would be triggered too
* late according to the dns_timeout and abort is
* necessary. */
if (ctx->dnsstarttime && ns_to_sec(now_ns) + ctx->cfg->dns_delay > ctx->dnsstarttime + ctx->cfg->dns_timeout) {
memprintf(&errmsg, "dns-01: Couldn't resolve the TXT records in %ds.", ctx->cfg->dns_timeout);
goto abort;
}
/* we don't need to wait, we can trigger the resolution
* after the delay */
st = ACME_RSLV_TRIGGER;