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:
parent
0f9b3daf98
commit
5555926fdd
@ -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
|
||||
----------
|
||||
|
@ -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 */
|
||||
|
98
src/acme.c
98
src/acme.c
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user