]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: acme: extend resolver-based DNS pre-check to dns-persist-01
authorWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 16:44:11 +0000 (18:44 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 16:45:08 +0000 (18:45 +0200)
Add challenge_type parameter to acme_rslv_start() to select the correct
DNS lookup prefix: _validation-persist.<domain> for dns-persist-01 and
_acme-challenge.<domain> for dns-01.

Default cond_ready to ACME_RDY_DNS|ACME_RDY_DELAY for dns-persist-01.
Extend ACME_CLI_WAIT to cover dns-persist-01 alongside dns-01.

In ACME_RSLV_READY, check only TXT record existence for dns-persist-01
since the resolver cannot parse multiple strings within a single TXT entry.

include/haproxy/acme_resolvers.h
src/acme.c
src/acme_resolvers.c

index fb3d7dff519e50668455aead9120b7ee8c18cd44..0a90abfa253c229c58a3b4d3cabb878f105ec87f 100644 (file)
@@ -10,7 +10,7 @@
 #include <haproxy/acme-t.h>
 #include <haproxy/resolvers-t.h>
 
-struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks, char **errmsg);
+struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks, const char *challenge_type, char **errmsg);
 void acme_rslv_free(struct acme_rslv *rslv);
 
 #endif
index c436b9979f0c06e6dc118f1907a9bcef83e18007..33fb8b6bd4600b01e8cfb70c84e23c5a249d6846 100644 (file)
@@ -441,6 +441,11 @@ static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx
                        cur_acme->cond_ready = ACME_RDY_CLI;
                }
 
+               /* dns-persist-01: wait then check for DNS propagation by default */
+               if ((strcasecmp("dns-persist-01", args[1]) == 0) && (cur_acme->cond_ready == 0)) {
+                       cur_acme->cond_ready = ACME_RDY_DNS | ACME_RDY_DELAY;
+               }
+
                if ((strcasecmp("http-01", args[1]) == 0) && (cur_acme->cond_ready != 0)) {
                        ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section, \"http-01\" is not compatible with the \"challenge-ready\" option\n", file, linenum, args[0], cursection);
                        err_code |= ERR_ALERT | ERR_FATAL;
@@ -2451,7 +2456,8 @@ re:
                                        goto retry;
                                }
                                if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
-                                       if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0 && ctx->cfg->cond_ready)
+                                       if ((strcasecmp(ctx->cfg->challenge, "dns-01") == 0 ||
+                                    strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) && ctx->cfg->cond_ready)
                                                st = ACME_CLI_WAIT;
                                        else
                                                st = ACME_CHALLENGE;
@@ -2572,7 +2578,7 @@ re:
 
                                HA_ATOMIC_INC(&ctx->dnstasks);
 
-                               auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, &errmsg);
+                               auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, ctx->cfg->challenge, &errmsg);
                                if (!auth->rslv)
                                        goto abort;
                                auth->rslv->acme_task = task;
@@ -2595,22 +2601,32 @@ re:
                        for (auth = ctx->auths; auth != NULL; auth = auth->next) {
                                if (auth->ready == ctx->cfg->cond_ready)
                                        continue;
-                               if (auth->rslv->result != RSLV_STATUS_VALID) {
-                                       send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: Couldn't get the TXT record for \"_acme-challenge.%.*s\", expected \"%.*s\" (status=%d)\n",
-                                                ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
-                                                (int)auth->token.len, auth->token.ptr,
-                                                auth->rslv->result);
-                                       all_ready = 0;
-                               } else {
-                                       if (isteq(auth->rslv->txt, auth->token)) {
+                               /* for dns-01, verify the TXT record content matches the
+                                * expected token. for dns-persist-01, only check that
+                                * the record exists since the resolver cannot read
+                                * multiple strings within a single TXT entry */
+                               if (auth->rslv->result == RSLV_STATUS_VALID) {
+                                       if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0) {
+                                               if (isteq(auth->rslv->txt, auth->token)) {
+                                                       auth->ready |= ACME_RDY_DNS;
+                                               } else {
+                                                       send_log(NULL, LOG_NOTICE,
+                                                       "acme: %s: dns-01: TXT record mismatch for \"_acme-challenge.%.*s\": expected \"%.*s\", got \"%.*s\"\n",
+                                                                ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
+                                                                (int)auth->token.len, auth->token.ptr,
+                                                                (int)auth->rslv->txt.len, auth->rslv->txt.ptr);
+                                                       all_ready = 0;
+                                               }
+                                       } else if (strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) {
                                                auth->ready |= ACME_RDY_DNS;
-                                       } else {
-                                               send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: TXT record mismatch for \"_acme-challenge.%.*s\": expected \"%.*s\", got \"%.*s\"\n",
-                                                        ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
-                                                        (int)auth->token.len, auth->token.ptr,
-                                                        (int)auth->rslv->txt.len, auth->rslv->txt.ptr);
-                                               all_ready = 0;
                                        }
+                               } else {
+                                       send_log(NULL, LOG_NOTICE, "acme: %s: %s: Couldn't get the TXT record for \"%s.%.*s\" (status=%d)\n",
+                                                ctx->store->path, ctx->cfg->challenge,
+                                                strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0 ? "_validation-persist" : "_acme-challenge",
+                                                (int)auth->dns.len, auth->dns.ptr,
+                                                auth->rslv->result);
+                                       all_ready = 0;
                                }
                                acme_rslv_free(auth->rslv);
                                auth->rslv = NULL;
index 65d03a36facf442ab4e443ea11b24ec997b8010f..3af73da604c9ee964f50d24a6145c9eeab6eccaf 100644 (file)
@@ -90,12 +90,13 @@ void acme_rslv_free(struct acme_rslv *rslv)
        free(rslv);
 }
 
-struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks, char **errmsg)
+struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks, const char *challenge_type, char **errmsg)
 {
        struct acme_rslv *rslv = NULL;
        struct resolvers *resolvers;
        char hostname[DNS_MAX_NAME_SIZE + 1];
        char dn[DNS_MAX_NAME_SIZE + 1];
+       const char *prefix;
        int hostname_len;
        int dn_len;
 
@@ -106,17 +107,22 @@ struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks
                goto error;
        }
 
-       /* dns-01 TXT record lives at _acme-challenge.<domain> */
-       hostname_len = snprintf(hostname, sizeof(hostname), "_acme-challenge.%.*s",
-                               (int)auth->dns.len, auth->dns.ptr);
+       /* dns-persist-01 TXT record lives at _validation-persist.<domain>,
+        * dns-01 TXT record lives at _acme-challenge.<domain> */
+       prefix = (strcasecmp(challenge_type, "dns-persist-01") == 0)
+                ? "_validation-persist"
+                : "_acme-challenge";
+
+       hostname_len = snprintf(hostname, sizeof(hostname), "%s.%.*s",
+                               prefix, (int)auth->dns.len, auth->dns.ptr);
        if (hostname_len < 0 || hostname_len >= (int)sizeof(hostname)) {
-               memprintf(errmsg, "hostname \"_acme-challenge.%.*s\" too long!\n", (int)auth->dns.len, auth->dns.ptr);
+               memprintf(errmsg, "hostname \"%s.%.*s\" too long!\n", prefix, (int)auth->dns.len, auth->dns.ptr);
                goto error;
        }
 
        dn_len = resolv_str_to_dn_label(hostname, hostname_len, dn, sizeof(dn));
        if (dn_len <= 0) {
-               memprintf(errmsg, "couldn't convert hostname \"_acme-challenge.%.*s\" into dn label\n", (int)auth->dns.len, auth->dns.ptr);
+               memprintf(errmsg, "couldn't convert hostname \"%s.%.*s\" into dn label\n", prefix, (int)auth->dns.len, auth->dns.ptr);
                goto error;
        }