]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: acme: use a map to store tokens and thumbprints
authorWilliam Lallemand <wlallemand@haproxy.com>
Tue, 29 Apr 2025 14:08:31 +0000 (16:08 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Tue, 29 Apr 2025 14:15:55 +0000 (16:15 +0200)
The stateless mode which was documented previously in the ACME example
is not convenient for all use cases.

First, when HAProxy generates the account key itself, you wouldn't be
able to put the thumbprint in the configuration, so you will have to get
the thumbprint and then reload.
Second, in the case you are using multiple account key, there are
multiple thumbprint, and it's not easy to know which one you want to use
when responding to the challenger.

This patch allows to configure a map in the acme section, which will be
filled by the acme task with the token corresponding to the challenge,
as the key, and the thumbprint as the value. This way it's easy to reply
the right thumbprint.

Example:
    http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].%[path,field(-1,/),map(virt@acme)]\n" if { path_beg '/.well-known/acme-challenge/' }

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

index 1a1f3a13af18ff8b521c0af364e77377284763eb..900f6635bc489efbd351417709180004d2390493 100644 (file)
@@ -5981,6 +5981,11 @@ keytype <string>
   or "ECDSA". You can also configure the "curves" for ECDSA and the number of
   "bits" for RSA. By default EC384 keys are generated.
 
+map <map>
+  Configure the map which will be used to store token (key) and thumbprint
+  (value), which is useful to reply to a challenge when there are multiple
+  account used. The acme task will add entries before validating the challenge
+  and will remove the entries at the end of the task.
 
 Example:
 
@@ -5991,7 +5996,7 @@ Example:
   frontend in
       bind *:80
       bind *:443 ssl
-      http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }
+      http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].%[path,field(-1,/),map(virt@acme)]\n" if { path_beg '/.well-known/acme-challenge/' }
       ssl-f-use crt "foo.example.com.pem.rsa"   acme LE1 domains "foo.example.com.pem,bar.example.com"
       ssl-f-use crt "foo.example.com.pem.ecdsa" acme LE2 domains "foo.example.com.pem,bar.example.com"
 
@@ -6002,6 +6007,7 @@ Example:
       challenge HTTP-01
       keytype RSA
       bits 2048
+      map virt@acme
 
   acme LE2
       directory https://acme-staging-v02.api.letsencrypt.org/directory
@@ -6010,6 +6016,7 @@ Example:
       challenge HTTP-01
       keytype ECDSA
       curves P-384
+      map virt@acme
 
 4. Proxies
 ----------
index 4689624178631bb52018161ff98a8d4a660d5ba6..8a3527df44f97845a521c92d36bac500bf741173 100644 (file)
@@ -13,6 +13,7 @@ struct acme_cfg {
        int linenum;                /* config linenum */
        char *name;                 /* section name */
        char *directory;            /* directory URL */
+       char *map;                  /* storage for tokens + thumbprint */
        struct {
                char *contact;      /* email associated to account */
                char *file;         /* account key filename */
index 31fc273e07edcea9e99df06de1a569beda59827a..fe84a2619be5d6c561f15d4770ecb08cf2dda12a 100644 (file)
@@ -27,6 +27,7 @@
 #include <haproxy/jws.h>
 #include <haproxy/list.h>
 #include <haproxy/log.h>
+#include <haproxy/pattern.h>
 #include <haproxy/ssl_ckch.h>
 #include <haproxy/ssl_sock.h>
 #include <haproxy/ssl_utils.h>
@@ -313,6 +314,22 @@ static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx
                        ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
                        goto out;
                }
+       } else if (strcmp(args[0], "map") == 0) {
+               /* save the map name for thumbprint + token storage */
+               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;
+
+               cur_acme->map = strdup(args[1]);
+               if (!cur_acme->map) {
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+                       goto out;
+               }
        } else if (*args[0] != 0) {
                ha_alert("parsing [%s:%d]: unknown keyword '%s' in '%s' section\n", file, linenum, args[0], cursection);
                err_code |= ERR_ALERT | ERR_FATAL;
@@ -552,6 +569,7 @@ static struct cfg_kw_list cfg_kws_acme = {ILH, {
        { CFG_ACME, "keytype",  cfg_parse_acme_cfg_key },
        { CFG_ACME, "bits",  cfg_parse_acme_cfg_key },
        { CFG_ACME, "curves",  cfg_parse_acme_cfg_key },
+       { CFG_ACME, "map",  cfg_parse_acme_kws },
        { 0, NULL, NULL },
 }};
 
@@ -614,6 +632,80 @@ static void acme_httpclient_end(struct httpclient *hc)
        task_wakeup(task, TASK_WOKEN_MSG);
 }
 
+/*
+ * Add a map entry with <challenge> as the key, and <thumprint> as value in the virt@acme map.
+ * Return 0 upon success or 1 otherwise.
+ */
+static int acme_add_challenge_map(const char *map, const char *challenge, const char *thumbprint, char **errmsg)
+{
+       int ret = 1;
+       struct pat_ref *ref;
+       struct pat_ref_elt *elt;
+
+       /* when no map configured, return without error */
+       if (!map)
+               return 0;
+
+       ref = pat_ref_lookup("virt@acme");
+       if (!ref) {
+               memprintf(errmsg, "Unknown map identifier 'virt@acme'.\n");
+               goto out;
+       }
+
+       HA_RWLOCK_WRLOCK(PATREF_LOCK, &ref->lock);
+       elt = pat_ref_load(ref, ref->curr_gen, challenge, thumbprint, -1, errmsg);
+       HA_RWLOCK_WRUNLOCK(PATREF_LOCK, &ref->lock);
+
+       if (elt == NULL)
+               goto out;
+
+       ret = 0;
+
+out:
+       return ret;
+}
+
+/*
+ * Remove the <challenge> from the virt@acme map
+ */
+static void acme_del_challenge_map(const char *map, const char *challenge)
+{
+       struct pat_ref *ref;
+
+       /* when no map configured, return without error */
+       if (!map)
+               return;
+
+       ref = pat_ref_lookup(map);
+       if (!ref)
+               goto out;
+
+       HA_RWLOCK_WRLOCK(PATREF_LOCK, &ref->lock);
+       pat_ref_delete(ref, challenge);
+       HA_RWLOCK_WRUNLOCK(PATREF_LOCK, &ref->lock);
+
+out:
+       return;
+}
+
+/*
+ * Remove all challenges from an acme_ctx from the virt@acme map
+ */
+static void acme_del_acme_ctx_map(const struct acme_ctx *ctx)
+{
+       struct acme_auth *auth;
+
+       /* when no map configured, return without error */
+       if (!ctx->cfg->map)
+               return;
+
+       auth = ctx->auths;
+       while (auth) {
+               acme_del_challenge_map(ctx->cfg->map, auth->token.ptr);
+               auth = auth->next;
+       }
+       return;
+}
 
 int acme_http_req(struct task *task, struct acme_ctx *ctx, struct ist url, enum http_meth_t meth, const struct http_hdr *hdrs, struct ist payload)
 {
@@ -1247,6 +1339,11 @@ int acme_res_auth(struct task *task, struct acme_ctx *ctx, struct acme_auth *aut
                        goto error;
                }
 
+               if (acme_add_challenge_map(ctx->cfg->map, auth->token.ptr, ctx->cfg->account.thumbprint, errmsg) != 0) {
+                       memprintf(errmsg, "couldn't add the token to virt@acme: %s", *errmsg);
+                       goto error;
+               }
+
                /* we only need one challenge, and iteration is only used to found the right one */
                break;
        }
@@ -1879,6 +1976,7 @@ abort:
        ha_free(&errmsg);
 
 end:
+       acme_del_acme_ctx_map(ctx);
        acme_ctx_destroy(ctx);
        task_destroy(task);
        task = NULL;