From: William Lallemand Date: Fri, 6 Dec 2024 16:44:53 +0000 (+0100) Subject: MEDIUM: ssl/cli: "show ssl sni" list the loaded SNI in frontends X-Git-Tag: v3.2-dev1~19 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5d1b30d6b84bc544b8eda846a632f2b55a19bb51;p=thirdparty%2Fhaproxy.git MEDIUM: ssl/cli: "show ssl sni" list the loaded SNI in frontends The "show ssl sni" command, allows one to dump the list of SNI in an haproxy process, or a designated frontend. It lists the SNI with the type, filename, and dates of expiration and activation --- diff --git a/doc/management.txt b/doc/management.txt index b9e3502fad..250cb6cbb6 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -3761,6 +3761,41 @@ show ssl providers - fips - base +show ssl sni [-f ] + Dump every SNI configured for the designated frontend, or all frontends if no + frontend was specified. It allows to see what SNI are offered for a frontend, + and to identify if a SNI is defined multiple time by multiple certificates for + the same frontend. + + Columns are separated by a single \t, allowing to parse it simply. + + The frontend/bind column shows the frontend name followed by the bind line + position in the configuration (file:linenum). + + The SNI column shows the SNI, it can be either a CN, a SAN or a positive + filter from a crt-list. Negative filters are not displayed. + + The 'type' column shows the encryption algorithm type, it can be "rsa", "ecdsa" or "dsa". + + The default certificates of a bind line, (which are either declared + explicitely by 'default-crt' or is implicitely the first certificate of a bind + line when no 'strict-sni' is used) shows the '*' character in the SNI column. + + The 'filename' column can be either a filename from the configuration, or an + alias declarated in a crt-store. + + The 'NotAfter' and 'NotBefore' columns are directly extracted from the X509 + leaf certificate. + + Example: + $ echo "@1 show ssl sni" | socat /var/run/haproxy-master.sock - | column -t -s $'\t' + # Frontend/Bind SNI Type Filename NotAfter NotBefore + li1/haproxy.cfg:10021 machine10 rsa machine10.pem.rsa Jun 13 13:37:21 2024 GMT May 14 13:37:21 2024 GMT + li1/haproxy.cfg:10021 machine10 ecdsa machine10.pem.ecdsa Jun 13 13:37:21 2024 GMT May 14 13:37:21 2024 GMT + li1/haproxy.cfg:10021 localhost rsa localhost.pem.rsa Jun 13 13:37:11 2024 GMT May 14 13:37:11 2024 GMT + li1/haproxy.cfg:10021 localhost ecdsa localhost.pem.ecdsa Jun 13 13:37:10 2024 GMT May 14 13:37:10 2024 GMT + li1/haproxy.cfg:10021 * rsa localhost.pem.rsa Jun 13 13:37:11 2024 GMT May 14 13:37:11 2024 GMT + show startup-logs Dump all messages emitted during the startup of the current haproxy process, each startup-logs buffer is unique to its haproxy worker. diff --git a/src/ssl_ckch.c b/src/ssl_ckch.c index 92ed13e65d..2966ba9675 100644 --- a/src/ssl_ckch.c +++ b/src/ssl_ckch.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +87,15 @@ struct show_cert_ctx { int transaction; }; +/* CLI context used by "show ssl sni" */ +struct show_sni_ctx { + struct proxy *px; + struct bind_conf *bind; + struct ebmb_node *n; + int nodetype; + int onefrontend; +}; + /* CLI context used by "dump ssl cert" */ struct dump_cert_ctx { struct ckch_store *ckchs; @@ -1535,6 +1545,177 @@ struct cert_exts cert_exts[] = { { NULL, CERT_TYPE_MAX, NULL }, }; +/* release function of the `show ssl sni' command */ +static void cli_release_show_sni(struct appctx *appctx) +{ + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); +} + +/* IO handler of "show ssl sni []". + * It makes use of a show_sni_ctx context + * + * The fonction does loop over the frontend, the bind_conf and the sni_ctx. + */ +static int cli_io_handler_show_sni(struct appctx *appctx) +{ + struct show_sni_ctx *ctx = appctx->svcctx; + struct buffer *trash = alloc_trash_chunk(); + struct ebmb_node *n = NULL; + int type = 0; + struct bind_conf *bind = NULL; + struct proxy *px = NULL; + + if (trash == NULL) + return 1; + + /* ctx->bind is NULL only once we finished dumping a frontend or when starting + * so let's dump the header in these cases*/ + if (ctx->bind == NULL && (ctx->onefrontend == 1 || (ctx->onefrontend == 0 && ctx->px == proxies_list))) + chunk_appendf(trash, "# Frontend/Bind\tSNI\tType\tFilename\tNotAfter\tNotBefore\n"); + if (applet_putchk(appctx, trash) == -1) + goto yield; + + for (px = ctx->px; px; px = px->next) { + + /* only check the frontends which are not internal proxies */ + if (!(px->cap & PR_CAP_FE) || (px->cap & PR_CAP_INT)) + continue; + + bind = ctx->bind; + /* if we didn't get a bind from the previous yield */ + if (!bind) + bind = LIST_ELEM(px->conf.bind.n, typeof(bind), by_fe); + + list_for_each_entry_from(bind, &px->conf.bind, by_fe) { + + HA_RWLOCK_RDLOCK(SNI_LOCK, &bind->sni_lock); + + /* do this twice: once for wildcards and once for standard SNI */ + for (type = ctx->nodetype; type < 2; type++) { + + n = ctx->n; /* get the node from previous yield */ + + if (!n) { + if (type == 0) + n = ebmb_first(&bind->sni_ctx); + else + n = ebmb_first(&bind->sni_w_ctx); + } + /* emty SNI tree, skip */ + if (!n) + continue; + + while (n) { + struct sni_ctx *sni; + const char *name; + const char *certalg; + + chunk_appendf(trash, "%s/%s:%d\t", bind->frontend->id, bind->file, bind->line); + + sni = ebmb_entry(n, struct sni_ctx, name); + + name = (char *)sni->name.key; + + chunk_appendf(trash, "%s%s%s\t", sni->neg ? "!" : "", type ? "*" : "", name); + + switch (sni->kinfo.sig) { + case TLSEXT_signature_ecdsa: + certalg = "ecdsa"; + break; + case TLSEXT_signature_rsa: + certalg = "rsa"; + break; + default: /* TLSEXT_signature_anonymous|dsa */ + certalg = "dsa"; + break; + } + + chunk_appendf(trash, "%s\t", certalg); + + /* we need to lock so the certificates in the ckch are not modified during the listing */ + chunk_appendf(trash, "%s\t", sni->ckch_inst->ckch_store->path); + chunk_appendf(trash, "%s\t", x509_get_notafter(sni->ckch_inst->ckch_store->data->cert)); + chunk_appendf(trash, "%s\n", x509_get_notbefore(sni->ckch_inst->ckch_store->data->cert)); + + if (applet_putchk(appctx, trash) == -1) { + HA_RWLOCK_RDUNLOCK(SNI_LOCK, &bind->sni_lock); + goto yield; + } + + n = ebmb_next(n); + } + ctx->n = NULL; + } + ctx->nodetype = 0; + HA_RWLOCK_RDUNLOCK(SNI_LOCK, &bind->sni_lock); + + } + ctx->bind = NULL; + /* only want to display the specified frontend */ + if (ctx->onefrontend) + break; + } + ctx->px = NULL; + + free_trash_chunk(trash); + return 1; +yield: + + ctx->px = px; + ctx->bind = bind; + ctx->n = n; + ctx->nodetype = type; + + free_trash_chunk(trash); + return 0; /* should come back */ +} + + +/* parsing function for 'show ssl sni [-f ]' */ +static int cli_parse_show_sni(char **args, char *payload, struct appctx *appctx, void *private) +{ + struct show_sni_ctx *ctx = applet_reserve_svcctx(appctx, sizeof(*ctx)); + + + ctx->px = proxies_list; + + /* look for the right to display */ + if (*args[3]) { + struct proxy *px; + + if (strcmp(args[3], "-f") != 0) + return cli_err(appctx, "'show ssl sni' only supports a '-f' option!\n"); + + if (*args[4] == '\0') + return cli_err(appctx, "'-f' requires a frontend name !\n"); + + for (px = proxies_list; px; px = px->next) { + + /* only check the frontends */ + if (!(px->cap & PR_CAP_FE)) + continue; + + /* skip the internal proxies */ + if (px->cap & PR_CAP_INT) + continue; + + if (strcmp(px->id, args[3]) == 0) { + ctx->px = px; + ctx->onefrontend = 1; + } + } + if (ctx->px == NULL) + goto error; + } + + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) + return cli_err(appctx, "Can't list SNIs\nOperations on certificates are currently locked!\n"); + + return 0; + +error: + return cli_err(appctx, "Couldn't find the specified frontend!\n"); +} /* release function of the `show ssl cert' command */ static void cli_release_show_cert(struct appctx *appctx) @@ -4182,6 +4363,8 @@ void ckch_deinit() /* register cli keywords */ static struct cli_kw_list cli_kws = {{ },{ + { { "show", "ssl", "sni", NULL }, "show ssl sni [-f ] : display the list of SNI and their corresponding filename", cli_parse_show_sni, cli_io_handler_show_sni, cli_release_show_sni }, + { { "new", "ssl", "cert", NULL }, "new ssl cert : create a new certificate file to be used in a crt-list or a directory", cli_parse_new_cert, NULL, NULL }, { { "set", "ssl", "cert", NULL }, "set ssl cert : replace a certificate file", cli_parse_set_cert, NULL, NULL }, { { "commit", "ssl", "cert", NULL }, "commit ssl cert : commit a certificate file", cli_parse_commit_cert, cli_io_handler_commit_cert, cli_release_commit_cert },