Compare commits

...

5 Commits

Author SHA1 Message Date
William Lallemand
a69203a3a9 DOC: configuration: document the "crt" frontend keyword
Document the "crt" keyword of frontend and listen section.
2025-02-17 18:23:38 +01:00
William Lallemand
af32dc61f4 MEDIUM: ssl/crtlist: "crt" keyword in frontend
This patch implements the "crt" keywords in frontend, declaring an
implicit crt-list named after the frontend.

The patch is split in two steps:

The first step is the crt keyword parser, which parses crt lines and
fill a "cfg_crt_node" struct containing a ssl_bind_conf and a
ckch_conf which are put in a list to be used later.

After parsing the frontend section, as a 2nd step, a
post_section_parser is called, it will create a crt-list named after
the frontend and will fill it with certificates from the list of
cfg_crt_node. Once created this crt-list will be loaded in every "ssl"
bind lines that didn't declare any crt or crt-list.

Example:

    listen https
       bind :443 ssl
       crt foobar.pem
       crt test1.net.crt key test1.net.key

Implements part of #2854
2025-02-17 18:23:38 +01:00
William Lallemand
5316923f6b MINOR: ssl/ckch: return from ckch_conf_clean() when conf is NULL
ckch_conf_clean() mustn't be executed when the argument is NULL, this
will keep the API more consistant like any free() function.
2025-02-17 18:23:38 +01:00
William Lallemand
5821eb062a MINOR: ssl/crtlist: handle crt_path == cc->crt in crtlist_load_crt()
Handle the case where crt_path == cc->crt, so the pointer doesn't get
free'd before getting strdup'ed in crtlist_load_crt().
2025-02-17 18:23:38 +01:00
William Lallemand
6e91a7872e MINOR: ssl/crtlist: split the ckch_conf loading from the crtlist line parsing
ckch_conf loading is not that simple as it requires to check
- if the cert already exists in the ckchs_tree
- if the ckch_conf is compatible with an existing cert in ckchs_tree
- if the cert is a bundle which need to load multiple ckch_store

This logic could be reuse elsewhere, so this commit introduce the new
crtlist_load_crt() function which does that.
2025-02-17 18:23:38 +01:00
5 changed files with 404 additions and 116 deletions

View File

@ -5783,6 +5783,7 @@ clitcpka-idle X X X -
clitcpka-intvl X X X -
compression X X X X
cookie X - X X
crt - X X -
declare capture - X X -
default-server X - X X
default_backend X X X -
@ -7009,6 +7010,62 @@ cookie <name> [ rewrite | insert | prefix ] [ indirect ] [ nocache ]
See also : "balance source", "capture cookie", "server" and "ignore-persist".
crt <crtname> [<sslbindconf> ...]*
Assignate a certificate to the current frontend.
May be used in the following contexts: tcp, http
May be used in sections : defaults | frontend | listen | backend
no | yes | yes | no
Arguments :
<sslbindconf> supports the following keywords from the bind line
(see Section 5.1. Bind options):
- allow-0rtt
- alpn
- ca-file
- ca-verify-file
- ciphers
- ciphersuites
- client-sigalgs
- crl-file
- curves
- ecdhe
- no-alpn
- no-ca-names
- npn
- sigalgs
- ssl-min-ver
- ssl-max-ver
- verify
sslbindconf also supports the following keywords from the crt-store load
keyword (see Section 3.12.1. Load options):
- key
- ocsp
- issuer
- sctl
- ocsp-update
Assignate a certificate <crtname> to a crt-list created automatically with the
frontend name and prefixed by @ (ex: '@frontend1').
This implicit crt-list will be assigned to every "ssl" bind lines in a
frontend that does not already have the "crt" or the "crt-list" line.
crt-list commands from the stats socket are effective with this crt-list, so
one could replace, remove or add certificates and SSL options to it.
Example :
frontend https
bind :443 ssl
crt foobar.pem.rsa sigalgs "RSA-PSS+SHA256"
crt test.foobar.pem
crt test2.foobar.crt key test2.foobar.key ocsp test2.foobar.ocsp ocsp-update on
See also : "crt-list" and "crt".
declare capture [ request | response ] len <length>
Declares a capture slot.

View File

@ -41,6 +41,7 @@ struct crtlist *crtlist_new(const char *filename, int unique);
int crtlist_parse_line(char *line, char **crt_path, struct crtlist_entry *entry, struct ckch_conf *conf, const char *file, int linenum, int from_cli, char **err);
int crtlist_parse_file(char *file, struct bind_conf *bind_conf, struct proxy *curproxy, struct crtlist **crtlist, char **err);
int crtlist_load_cert_dir(char *path, struct bind_conf *bind_conf, struct crtlist **crtlist, char **err);
int crtlist_load_crt(char *crt_path, struct ckch_conf *cc, struct crtlist *newlist, struct crtlist_entry *entry, char *file, int linenum, char **err);
void crtlist_deinit();

View File

@ -30,6 +30,8 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <import/ebsttree.h>
#include <haproxy/api.h>
#include <haproxy/base64.h>
#include <haproxy/cfgparse.h>
@ -40,7 +42,9 @@
#include <haproxy/ssl_utils.h>
#include <haproxy/tools.h>
#include <haproxy/ssl_ckch.h>
#include <haproxy/ssl_crtlist.h>
#include <haproxy/ssl_ocsp.h>
#include <haproxy/ssl_sock.h>
/****************** Global Section Parsing ********************************************/
@ -2153,7 +2157,205 @@ static int ssl_parse_skip_self_issued_ca(char **args, int section_type, struct p
#endif
}
struct cfg_crt_node {
int linenum;
char *filename;
struct ssl_bind_conf *ssl_conf;
struct ckch_conf *ckch_conf;
struct list list;
};
/* list used for inline crt-list initialization */
static struct list cur_crtlist = LIST_HEAD_INIT(cur_crtlist);
/*
* Parse a "crt" line in a frontend.
*/
static int proxy_parse_crt(char **args, int section_type, struct proxy *curpx,
const struct proxy *defpx, const char *file, int linenum,
char **err)
{
int cfgerr = 0;
struct ssl_bind_conf *ssl_conf = NULL;
struct ckch_conf *ckch_conf = NULL;
struct cfg_crt_node *cfg_crt_node = NULL;
int cur_arg = 2;
int i;
if (!*args[1]) {
memprintf(err, "parsing [%s:%d] : '%s' : expects a certificate name", file, linenum, args[0]);
goto error;
}
cfg_crt_node = calloc(1, sizeof *cfg_crt_node);
if (!cfg_crt_node) {
memprintf(err, "not enough memory!");
goto error;
}
ckch_conf = calloc(1, sizeof *ckch_conf);
if (!ckch_conf) {
memprintf(err, "not enough memory!");
goto error;
}
ckch_conf->crt = strdup(args[1]);
if (!ckch_conf->crt) {
memprintf(err, "not enough memory!");
goto error;
}
while (*args[cur_arg]) {
int found = 0;
/* first look for crt-list keywords */
for (i = 0; ssl_crtlist_kws[i].kw != NULL; i++) {
if (strcmp(ssl_crtlist_kws[i].kw, args[cur_arg]) == 0) {
if (!ssl_conf)
ssl_conf = calloc(1, sizeof *ssl_conf);
if (!ssl_conf) {
memprintf(err, "not enough memory!");
goto error;
}
cfgerr |= ssl_crtlist_kws[i].parse(args, cur_arg, NULL, ssl_conf, 0, err);
if (cfgerr & ERR_CODE)
goto error;
cur_arg += 1 + ssl_crtlist_kws[i].skip;
found = 1;
goto next;
}
}
/* then look for ckch_conf keywords */
cfgerr |= ckch_conf_parse(args, cur_arg, ckch_conf, &found, file, linenum, err);
if (cfgerr & ERR_CODE)
goto error;
if (found) {
cur_arg += 2; /* skip 2 words if the keyword was found */
ckch_conf->used = CKCH_CONF_SET_CRTLIST; /* if they are options they must be used everywhere */
goto next;
}
next:
if (!found) {
memprintf(err, "unknown crt keyword '%s'", args[cur_arg]);
goto error;
}
}
cfg_crt_node->ssl_conf = ssl_conf;
cfg_crt_node->ckch_conf = ckch_conf;
LIST_INSERT(&cur_crtlist, &cfg_crt_node->list);
return 0;
error:
ckch_conf_clean(ckch_conf);
ha_free(&ckch_conf);
ssl_sock_free_ssl_conf(ssl_conf);
ha_free(&ssl_conf);
ha_free(&cfg_crt_node);
return -1;
}
/*
* After parsing the crt keywords in a frontend/listen section, create the corresponding crt-list and initialize the
* certificates
*/
static int post_section_frontend_crt_init()
{
struct crtlist *newlist = NULL;
struct crtlist_entry *entry = NULL;
int err_code = 0;
struct cfg_crt_node *n, *r;
struct bind_conf *b;
char *crtlist_name = NULL;
char *err = NULL;
list_for_each_entry_safe(n, r, &cur_crtlist, list) {
/* create a new crt-list with the frontend name or a specified name */
if (!crtlist_name)
memprintf(&crtlist_name, "@%s", curproxy->id);
if (!crtlist_name) {
memprintf(&err, "Not enough memory!");
err_code |= ERR_ALERT | ERR_FATAL;
goto error;
}
if (!newlist)
newlist = crtlist_new(crtlist_name, 0);
if (!newlist) {
memprintf(&err, "Not enough memory!");
err_code |= ERR_ALERT | ERR_FATAL;
goto error;
}
entry = crtlist_entry_new();
if (entry == NULL) {
memprintf(&err, "Not enough memory!");
err_code |= ERR_ALERT | ERR_FATAL;
goto error;
}
/* must set the ssl_conf in case of duplication of the crtlist_entry */
entry->ssl_conf = n->ssl_conf;
err_code |= crtlist_load_crt(n->ckch_conf->crt, n->ckch_conf, newlist, entry, n->filename, n->linenum, &err);
if (err_code & ERR_CODE)
goto error;
LIST_DELETE(&n->list);
/* n->ssl_conf is reused so we don't free them here */
free(n->ckch_conf);
free(n);
}
if (newlist) {
if (ebst_insert(&crtlists_tree, &newlist->node) != &newlist->node) {
memprintf(&err, "Couldn't create the crt-list '%s', this name is already used by another crt-list!", crtlist_name);
err_code |= ERR_ALERT | ERR_FATAL;
goto error;
}
/* look for "ssl" bind lines without any crt nor crt-line */
list_for_each_entry(b, &curproxy->conf.bind, by_fe) {
if (b->options & BC_O_USE_SSL) {
if (eb_is_empty(&b->sni_ctx) && eb_is_empty(&b->sni_w_ctx)) {
err_code |= ssl_sock_load_cert_list_file(crtlist_name, 0, b, curproxy, &err);
if (err_code & ERR_CODE)
goto error;
}
}
}
}
return err_code;
error:
if (err)
ha_alert("%s.\n", err);
free(err);
list_for_each_entry_safe(n, r, &cur_crtlist, list) {
ckch_conf_clean(n->ckch_conf);
ha_free(&n->ckch_conf);
ssl_sock_free_ssl_conf(n->ssl_conf);
ha_free(&n->ssl_conf);
LIST_DELETE(&n->list);
ha_free(&n);
}
ha_free(&crtlist_name);
crtlist_entry_free(entry);
crtlist_free(newlist);
return err_code;
}
REGISTER_CONFIG_POST_SECTION("listen", post_section_frontend_crt_init);
REGISTER_CONFIG_POST_SECTION("frontend", post_section_frontend_crt_init);
/* Note: must not be declared <const> as its list will be overwritten.
@ -2343,6 +2545,9 @@ static struct cfg_kw_list cfg_kws = {ILH, {
{ CFG_GLOBAL, "ssl-default-server-ciphersuites", ssl_parse_global_ciphersuites },
{ CFG_GLOBAL, "ssl-load-extra-files", ssl_parse_global_extra_files },
{ CFG_GLOBAL, "ssl-load-extra-del-ext", ssl_parse_global_extra_noext },
{ CFG_LISTEN, "crt", proxy_parse_crt },
{ 0, NULL, NULL },
}};

View File

@ -4841,6 +4841,9 @@ out:
/* freeing the content of a ckch_conf structure */
void ckch_conf_clean(struct ckch_conf *conf)
{
if (!conf)
return;
free(conf->crt);
free(conf->key);
free(conf->ocsp);

View File

@ -501,6 +501,141 @@ error:
return cfgerr;
}
/*
* Look for a ckch_store <crt_path> which is a compatible with <cc>
* Or create a new ckch_store if none exists with this name.
*
* If the file is a bundle, then duplicate the entries
* Then insert the entries in the list
*/
int crtlist_load_crt(char *crt_path, struct ckch_conf *cc, struct crtlist *newlist, struct crtlist_entry *entry, char *file, int linenum, char **err)
{
struct ckch_store *ckchs;
int found = 0;
struct stat st;
int cfgerr = 0;
/* Look for a ckch_store or create one */
ckchs = ckchs_lookup(crt_path);
if (ckchs == NULL) {
if (stat(crt_path, &st) == 0) {
found++;
if (crt_path != cc->crt) {
free(cc->crt);
cc->crt = strdup(crt_path);
if (cc->crt == NULL) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
}
ckchs = ckch_store_new_load_files_conf(crt_path, cc, err);
if (ckchs == NULL) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
ckchs->conf = *cc;
entry->node.key = ckchs;
entry->crtlist = newlist;
ebpt_insert(&newlist->entries, &entry->node);
LIST_APPEND(&newlist->ord_entries, &entry->by_crtlist);
LIST_APPEND(&ckchs->crtlist_entry, &entry->by_ckch_store);
} else if (global_ssl.extra_files & SSL_GF_BUNDLE) {
/* If we didn't find the file, this could be a
bundle, since 2.3 we don't support multiple
certificate in the same OpenSSL store, so we
emulate it by loading each file separately. To
do so we need to duplicate the entry in the
crt-list because it becomes independent */
char fp[MAXPATHLEN+1] = {0};
int n = 0;
struct crtlist_entry *entry_dup = entry; /* use the previous created entry */
for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
int ret;
ret = snprintf(fp, sizeof(fp), "%s.%s", crt_path, SSL_SOCK_KEYTYPE_NAMES[n]);
if (ret > sizeof(fp))
continue;
ckchs = ckchs_lookup(fp);
if (!ckchs) {
if (stat(fp, &st) == 0) {
if (cc->used) {
memprintf(err, "%sCan't load '%s'. Using crt-store keyword is not compatible with multi certificates bundle.\n",
err && *err ? *err : "", crt_path);
cfgerr |= ERR_ALERT | ERR_FATAL;
}
ckchs = ckch_store_new_load_files_path(fp, err);
if (!ckchs) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
} else {
continue; /* didn't find this extension, skip */
}
}
found++;
linenum++; /* we duplicate the line for this entry in the bundle */
if (!entry_dup) { /* if the entry was used, duplicate one */
linenum++;
entry_dup = crtlist_entry_dup(entry);
if (!entry_dup) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
entry_dup->linenum = linenum;
}
entry_dup->node.key = ckchs;
entry_dup->crtlist = newlist;
ebpt_insert(&newlist->entries, &entry_dup->node);
LIST_APPEND(&newlist->ord_entries, &entry_dup->by_crtlist);
LIST_APPEND(&ckchs->crtlist_entry, &entry_dup->by_ckch_store);
entry_dup = NULL; /* the entry was used, we need a new one next round */
}
#if HA_OPENSSL_VERSION_NUMBER < 0x10101000L
if (found) {
memprintf(err, "%sCan't load '%s'. Loading a multi certificates bundle requires OpenSSL >= 1.1.1\n",
err && *err ? *err : "", crt_path);
cfgerr |= ERR_ALERT | ERR_FATAL;
}
#endif
}
if (!found) {
memprintf(err, "%sunable to stat SSL certificate from file '%s' : %s.\n",
err && *err ? *err : "", crt_path, strerror(errno));
cfgerr |= ERR_ALERT | ERR_FATAL;
}
} else {
if (ckch_conf_cmp(&ckchs->conf, cc, err) != 0) {
memprintf(err, "'%s' in crt-list '%s' line %d, is already defined with incompatible parameters:\n %s", crt_path, file, linenum, err ? *err : "");
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
entry->node.key = ckchs;
entry->crtlist = newlist;
ebpt_insert(&newlist->entries, &entry->node);
LIST_APPEND(&newlist->ord_entries, &entry->by_crtlist);
LIST_APPEND(&ckchs->crtlist_entry, &entry->by_ckch_store);
found++;
}
entry = NULL;
error:
return cfgerr;
}
/* This function parse a crt-list file and store it in a struct crtlist, each line is a crtlist_entry structure
@ -514,7 +649,6 @@ int crtlist_parse_file(char *file, struct bind_conf *bind_conf, struct proxy *cu
struct crtlist_entry *entry = NULL;
char thisline[CRT_LINESIZE];
FILE *f;
struct stat buf;
int linenum = 0;
int cfgerr = 0;
int missing_lf = -1;
@ -536,9 +670,7 @@ int crtlist_parse_file(char *file, struct bind_conf *bind_conf, struct proxy *cu
char *line = thisline;
char *crt_path;
char path[MAXPATHLEN+1];
struct ckch_store *ckchs;
struct ckch_conf cc = {};
int found = 0;
if (missing_lf != -1) {
memprintf(err, "parsing [%s:%d]: Stray NUL character at position %d.\n",
@ -601,120 +733,10 @@ int crtlist_parse_file(char *file, struct bind_conf *bind_conf, struct proxy *cu
crt_path = path;
}
/* Look for a ckch_store or create one */
ckchs = ckchs_lookup(crt_path);
if (ckchs == NULL) {
if (stat(crt_path, &buf) == 0) {
found++;
free(cc.crt);
cc.crt = strdup(crt_path);
if (cc.crt == NULL) {
cfgerr |= ERR_ALERT | ERR_FATAL;
cfgerr |= crtlist_load_crt(crt_path, &cc, newlist, entry, file, linenum, err);
if (cfgerr & ERR_CODE)
goto error;
}
ckchs = ckch_store_new_load_files_conf(crt_path, &cc, err);
if (ckchs == NULL) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
ckchs->conf = cc;
entry->node.key = ckchs;
entry->crtlist = newlist;
ebpt_insert(&newlist->entries, &entry->node);
LIST_APPEND(&newlist->ord_entries, &entry->by_crtlist);
LIST_APPEND(&ckchs->crtlist_entry, &entry->by_ckch_store);
} else if (global_ssl.extra_files & SSL_GF_BUNDLE) {
/* If we didn't find the file, this could be a
bundle, since 2.3 we don't support multiple
certificate in the same OpenSSL store, so we
emulate it by loading each file separately. To
do so we need to duplicate the entry in the
crt-list because it becomes independent */
char fp[MAXPATHLEN+1] = {0};
int n = 0;
struct crtlist_entry *entry_dup = entry; /* use the previous created entry */
for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
struct stat buf;
int ret;
ret = snprintf(fp, sizeof(fp), "%s.%s", crt_path, SSL_SOCK_KEYTYPE_NAMES[n]);
if (ret > sizeof(fp))
continue;
ckchs = ckchs_lookup(fp);
if (!ckchs) {
if (stat(fp, &buf) == 0) {
if (cc.used) {
memprintf(err, "%sCan't load '%s'. Using crt-store keyword is not compatible with multi certificates bundle.\n",
err && *err ? *err : "", crt_path);
cfgerr |= ERR_ALERT | ERR_FATAL;
}
ckchs = ckch_store_new_load_files_path(fp, err);
if (!ckchs) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
} else {
continue; /* didn't find this extension, skip */
}
}
found++;
linenum++; /* we duplicate the line for this entry in the bundle */
if (!entry_dup) { /* if the entry was used, duplicate one */
linenum++;
entry_dup = crtlist_entry_dup(entry);
if (!entry_dup) {
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
entry_dup->linenum = linenum;
}
entry_dup->node.key = ckchs;
entry_dup->crtlist = newlist;
ebpt_insert(&newlist->entries, &entry_dup->node);
LIST_APPEND(&newlist->ord_entries, &entry_dup->by_crtlist);
LIST_APPEND(&ckchs->crtlist_entry, &entry_dup->by_ckch_store);
entry_dup = NULL; /* the entry was used, we need a new one next round */
}
#if HA_OPENSSL_VERSION_NUMBER < 0x10101000L
if (found) {
memprintf(err, "%sCan't load '%s'. Loading a multi certificates bundle requires OpenSSL >= 1.1.1\n",
err && *err ? *err : "", crt_path);
cfgerr |= ERR_ALERT | ERR_FATAL;
}
#endif
}
if (!found) {
memprintf(err, "%sunable to stat SSL certificate from file '%s' : %s.\n",
err && *err ? *err : "", crt_path, strerror(errno));
cfgerr |= ERR_ALERT | ERR_FATAL;
}
} else {
if (ckch_conf_cmp(&ckchs->conf, &cc, err) != 0) {
memprintf(err, "'%s' in crt-list '%s' line %d, is already defined with incompatible parameters:\n %s", crt_path, file, linenum, err ? *err : "");
cfgerr |= ERR_ALERT | ERR_FATAL;
goto error;
}
entry->node.key = ckchs;
entry->crtlist = newlist;
ebpt_insert(&newlist->entries, &entry->node);
LIST_APPEND(&newlist->ord_entries, &entry->by_crtlist);
LIST_APPEND(&ckchs->crtlist_entry, &entry->by_ckch_store);
found++;
}
entry = NULL;
}
if (missing_lf != -1) {