From: William Lallemand Date: Mon, 20 Apr 2026 16:06:43 +0000 (+0200) Subject: MINOR: acme: allow IP SAN in certificate request X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=95c400d08ec442606e0df4aaf0fbf57459d18bc9;p=thirdparty%2Fhaproxy.git MINOR: acme: allow IP SAN in certificate request Implement IP in both requestOrder and CSR so a certificate with SAN IPs can be generated. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 4e6cdcbea..f6f0c4a2c 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -32193,6 +32193,19 @@ domains load crt "example.com.pem" acme LE domains "bar.example.com,foo.example.com" +ips + Configure the list of IP addresses that will be included as IP SANs in the + ACME certificate. IP addresses are separated by commas in the list. + + Generating a certificate with IPs might require the use of the "shortlived" + profile. + + See also Section 12.8 ("ACME"), "acme" and "domains" in this section. + + Example: + + load crt "server.pem" acme LE ips "192.0.2.1,2001:db8::1" + key This argument is optional. Load a private key in PEM format. If a private key was already defined in "crt", it will overwrite it. diff --git a/include/haproxy/ssl_ckch-t.h b/include/haproxy/ssl_ckch-t.h index f2024f082..26f468a08 100644 --- a/include/haproxy/ssl_ckch-t.h +++ b/include/haproxy/ssl_ckch-t.h @@ -72,6 +72,7 @@ struct ckch_conf { struct { char *id; char **domains; + char **ips; } acme; struct { struct { diff --git a/src/acme.c b/src/acme.c index 8b4268b3f..b2e60de74 100644 --- a/src/acme.c +++ b/src/acme.c @@ -2029,7 +2029,9 @@ int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg) { IST_NULL, IST_NULL } }; int ret = 1; + int first = 1; char **san = ctx->store->conf.acme.domains; + char **ip = ctx->store->conf.acme.ips; if ((req_in = alloc_trash_chunk()) == NULL) goto error; @@ -2038,12 +2040,16 @@ int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg) chunk_printf(req_in, "{ \"identifiers\": [ "); - if (!san) + if (!san && !ip) goto error; for (; san && *san; san++) { -// fprintf(stderr, "%s:%d %s\n", __FUNCTION__, __LINE__, *san); - chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", (*san == *ctx->store->conf.acme.domains) ? "" : ",", *san); + chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", first ? "" : ",", *san); + first = 0; + } + for (; ip && *ip; ip++) { + chunk_appendf(req_in, "%s{ \"type\": \"ip\", \"value\": \"%s\" }", first ? "" : ",", *ip); + first = 0; } chunk_appendf(req_in, " ]"); @@ -3038,7 +3044,7 @@ end: /* * Generate a X509_REQ using a PKEY and a list of SAN finished by a NULL entry */ -X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san) +X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san, char **ips) { struct buffer *san_trash = NULL; X509_REQ *x = NULL; @@ -3060,9 +3066,9 @@ X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san) if ((nm = X509_NAME_new()) == NULL) goto error; - /* common name is the first SAN in the list */ + /* common name is the first domain, or the first IP if no domain */ if (!X509_NAME_add_entry_by_txt(nm, "CN", MBSTRING_ASC, - (unsigned char *)san[0], -1, -1, 0)) + (unsigned char *)(san ? san[0] : ips[0]), -1, -1, 0)) goto error; /* assign the CN to the REQ */ if (!X509_REQ_set_subject_name(x, nm)) @@ -3072,8 +3078,11 @@ X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san) if ((exts = sk_X509_EXTENSION_new_null()) == NULL) goto error; - for (i = 0; san[i]; i++) { - chunk_appendf(san_trash, "%sDNS:%s", i ? "," : "", san[i]); + for (i = 0; san && san[i]; i++) { + chunk_appendf(san_trash, "%sDNS:%s", san_trash->data ? "," : "", san[i]); + } + for (i = 0; ips && ips[i]; i++) { + chunk_appendf(san_trash, "%sIP:%s", san_trash->data ? "," : "", ips[i]); } if ((str_san = my_strndup(san_trash->area, san_trash->data)) == NULL) goto error; @@ -3159,8 +3168,8 @@ static int acme_start_task(struct ckch_store *store, char **errmsg) struct ckch_store *newstore = NULL; EVP_PKEY *pkey = NULL; - if (!store->conf.acme.domains) { - memprintf(errmsg, "No 'domains' were configured for certificate. "); + if (!store->conf.acme.domains && !store->conf.acme.ips) { + memprintf(errmsg, "No 'domains' or 'ips' were configured for certificate. "); goto err; } @@ -3211,7 +3220,7 @@ static int acme_start_task(struct ckch_store *store, char **errmsg) pkey = NULL; } - ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains); + ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains, store->conf.acme.ips); if (!ctx->req) { memprintf(errmsg, "%sCan't generate a CSR.", *errmsg ? *errmsg : ""); goto err; diff --git a/src/ssl_ckch.c b/src/ssl_ckch.c index c03e95d36..122d314e6 100644 --- a/src/ssl_ckch.c +++ b/src/ssl_ckch.c @@ -1112,6 +1112,26 @@ struct ckch_store *ckchs_dup(const struct ckch_store *src) dst->conf.acme.domains = r; } + if (src->conf.acme.ips) { + r = NULL; + n = 0; + + /* copy the array of IP strings */ + + while (src->conf.acme.ips[n]) { + r = realloc(r, sizeof(char *) * (n + 2)); + if (!r) + goto error; + + r[n] = strdup(src->conf.acme.ips[n]); + if (!r[n]) + goto error; + n++; + } + r[n] = 0; + dst->conf.acme.ips = r; + } + return dst; error: @@ -4904,6 +4924,7 @@ struct ckch_conf_kws ckch_conf_kws[] = { { "acme", offsetof(struct ckch_conf, acme.id), PARSE_TYPE_STR, ckch_conf_acme_init, }, #endif { "domains", offsetof(struct ckch_conf, acme.domains), PARSE_TYPE_ARRAY_SUBSTR, NULL, }, + { "ips", offsetof(struct ckch_conf, acme.ips), PARSE_TYPE_ARRAY_SUBSTR, NULL, }, { "generate-dummy", offsetof(struct ckch_conf, gencrt.on), PARSE_TYPE_ONOFF, NULL, }, { "keytype", offsetof(struct ckch_conf, gencrt.key.type), PARSE_TYPE_STR, NULL, }, { "bits", offsetof(struct ckch_conf, gencrt.key.bits), PARSE_TYPE_INT, NULL, }, @@ -5276,6 +5297,14 @@ void ckch_conf_clean(struct ckch_conf *conf) } ha_free(&conf->acme.domains); + r = conf->acme.ips; + while (r && *r) { + char *prev = *r; + r++; + free(prev); + } + ha_free(&conf->acme.ips); + } static char current_crtstore_name[PATH_MAX] = {};