]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: acme: add 'dns-timeout' keyword for dns-01 challenge
authorWilliam Lallemand <wlallemand@haproxy.com>
Wed, 1 Apr 2026 16:30:06 +0000 (18:30 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Wed, 1 Apr 2026 16:56:13 +0000 (18:56 +0200)
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.

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

index 7d6b1d86e35de445b0de4b6b9e5eb37bde14be7f..f579bc157957b2c260833d85de2da4c12a671a4f 100644 (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
index bee520cf54d81a7914e07e6ffc41ca7f2dc5fff7..86b2256aa862c6150db3d0567b4f62158813e9bb 100644 (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];
index a688f42afea2e187dbc2faa9c587c87fce49ad58..a1e88250f576e808c5ae14af5399bd59cafbebc3 100644 (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;