From: William Lallemand Date: Tue, 29 Apr 2025 14:08:31 +0000 (+0200) Subject: MEDIUM: acme: use a map to store tokens and thumbprints X-Git-Tag: v3.2-dev13~40 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5555926fdd3c844500525d677f9c35488555e87a;p=thirdparty%2Fhaproxy.git MEDIUM: acme: use a map to store tokens and thumbprints 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/' } --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 1a1f3a13a..900f6635b 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -5981,6 +5981,11 @@ keytype or "ECDSA". You can also configure the "curves" for ECDSA and the number of "bits" for RSA. By default EC384 keys are generated. +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 ---------- diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h index 468962417..8a3527df4 100644 --- a/include/haproxy/acme-t.h +++ b/include/haproxy/acme-t.h @@ -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 */ diff --git a/src/acme.c b/src/acme.c index 31fc273e0..fe84a2619 100644 --- a/src/acme.c +++ b/src/acme.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -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 as the key, and 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 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;