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/' }
This commit is contained in:
William Lallemand 2025-04-29 16:08:31 +02:00
parent 0f9b3daf98
commit 5555926fdd
3 changed files with 107 additions and 1 deletions

View 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
----------

View 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 */

View 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;