]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: acme: allow IP SAN in certificate request master
authorWilliam Lallemand <wlallemand@haproxy.com>
Mon, 20 Apr 2026 16:06:43 +0000 (18:06 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 20 Apr 2026 16:10:47 +0000 (18:10 +0200)
Implement IP in both requestOrder and CSR so a certificate with SAN IPs
can be generated.

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

index 4e6cdcbea6864a12f00cb88a48bcfe1dbcf44e1c..f6f0c4a2c6eefcb65b8c0be0e86e44cc0859dd3e 100644 (file)
@@ -32193,6 +32193,19 @@ domains <string>
 
     load crt "example.com.pem" acme LE domains "bar.example.com,foo.example.com"
 
+ips <string>
+  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 <filename>
   This argument is optional. Load a private key in PEM format. If a private key
   was already defined in "crt", it will overwrite it.
index f2024f082ceb424816eb6ed981f920057180e5a9..26f468a08590588ab4a321ff08373cbe3b232d10 100644 (file)
@@ -72,6 +72,7 @@ struct ckch_conf {
        struct {
                char *id;
                char **domains;
+               char **ips;
        } acme;
        struct {
                struct {
index 8b4268b3fbf5199a3376e1eb7959d79737e74f62..b2e60de74fc91a8297787db7a8781192648730fa 100644 (file)
@@ -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;
index c03e95d36a476da65c9545a10011010cc74e6ee8..122d314e6e12e250f813d197198a375ebfdc0fe6 100644 (file)
@@ -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] = {};