From 478dd7bad0be11fa21be23aa13a8398dce174a3f Mon Sep 17 00:00:00 2001 From: Remi Tricot-Le Breton Date: Tue, 28 Oct 2025 18:00:45 +0100 Subject: [PATCH] MEDIUM: ssl: Add certificate password callback that calls external command When a certificate is protected by a password, we can provide the password via the dedicated pem_password_cb param provided to PEM_read_bio_PrivateKey. HAProxy will fetch the password automatically during init by calling a user-defined external command that should dump the right password on its standard output (see new 'ssl-passphrase-cmd' global option). --- doc/configuration.txt | 12 +++++ include/haproxy/ssl_sock-t.h | 7 +++ include/haproxy/ssl_sock.h | 1 + src/cfgparse-ssl.c | 61 ++++++++++++++++++++++ src/ssl_ckch.c | 3 +- src/ssl_sock.c | 98 ++++++++++++++++++++++++++++++++++++ 6 files changed, 181 insertions(+), 1 deletion(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 136a7b749..1d53b4151 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -3339,6 +3339,18 @@ ssl-dh-param-file "openssl dhparam ", where size should be at least 2048, as 1024-bit DH parameters should not be considered secure anymore. +ssl-passphrase-cmd ... + This settings is only available when support for OpenSSL was built in. It + allows to define a full command line that will be called when an encrypted + certificate is loaded during init. The command could be a script or any other + program. It will be provided with the encrypted private key path as first + parameter and the user-defined "args" parameters then and should dump the + passphrase that allows to decode the encrypted private key on the standard + output. + For every new encrypted private key loaded during init, HAProxy will first + try every other already known passphrase to decode the private key and will + ultimately call the passphrase command again if none works. + ssl-propquery This setting is only available when support for OpenSSL was built in and when OpenSSL's version is at least 3.0. It allows to define a default property diff --git a/include/haproxy/ssl_sock-t.h b/include/haproxy/ssl_sock-t.h index d8c261388..c076f7fb5 100644 --- a/include/haproxy/ssl_sock-t.h +++ b/include/haproxy/ssl_sock-t.h @@ -336,6 +336,8 @@ struct global_ssl { #endif int renegotiate; /* Renegotiate mode (SSL_RENEGOTIATE_ flag) */ + char **passphrase_cmd; + int passphrase_cmd_args_cnt; }; /* The order here matters for picking a default context, @@ -355,5 +357,10 @@ struct ssl_counters { long long failed_ocsp_staple; }; +struct passphrase_cb_data { + const char *path; + int passphrase_idx; +}; + #endif /* USE_OPENSSL */ #endif /* _HAPROXY_SSL_SOCK_T_H */ diff --git a/include/haproxy/ssl_sock.h b/include/haproxy/ssl_sock.h index 4658ab9c6..d421061ca 100644 --- a/include/haproxy/ssl_sock.h +++ b/include/haproxy/ssl_sock.h @@ -132,6 +132,7 @@ struct issuer_chain* ssl_get0_issuer_chain(X509 *cert); int ssl_load_global_issuer_from_BIO(BIO *in, char *fp, char **err); int ssl_sock_load_cert(char *path, struct bind_conf *bind_conf, int is_default, char **err); int ssl_sock_load_srv_cert(char *path, struct server *server, int create_if_none, char **err); +int ssl_sock_passwd_cb(char *buf, int size, int rwflag, void *userdata); void ssl_free_global_issuers(void); int ssl_initialize_random(void); int ssl_sock_load_cert_list_file(char *file, int dir, struct bind_conf *bind_conf, struct proxy *curproxy, char **err); diff --git a/src/cfgparse-ssl.c b/src/cfgparse-ssl.c index 297809780..3b694def0 100644 --- a/src/cfgparse-ssl.c +++ b/src/cfgparse-ssl.c @@ -676,6 +676,65 @@ static int ssl_parse_global_extra_noext(char **args, int section_type, struct pr } +/* parse 'ssl-passphrase-cmd' */ +static int ssl_parse_global_passphrase_cmd(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + int arg_cnt = 0; + int i; + + if (!*args[1]) { + memprintf(err, "global statement '%s' expects a command line to a passphrase-providing tool (script/binary...) and its arguments.", args[0]); + return 1; + } + + for (; *args[arg_cnt + 2]; ++arg_cnt) + ; + + /* The first argument, by convention, should point to the filename + * associated with the file being executed. The array of pointers must + * be terminated by a null pointer. + * The certificate path will also be passed as first arg so we must + * leave enough space . + */ + global_ssl.passphrase_cmd_args_cnt = arg_cnt + 1 + 1 + 1; + + global_ssl.passphrase_cmd = calloc(global_ssl.passphrase_cmd_args_cnt, sizeof(*global_ssl.passphrase_cmd)); + if (!global_ssl.passphrase_cmd) { + memprintf(err, "'%s' : Could not allocate memory", args[0]); + return ERR_ALERT | ERR_FATAL; + } + + global_ssl.passphrase_cmd[0] = strdup(args[1]); + + if (!global_ssl.passphrase_cmd[0]) { + memprintf(err, "'%s' : Could not allocate memory", args[0]); + goto err_alloc; + } + + for (i = 0; i < arg_cnt; ++i) { + /* The first two slots have a special use, they will contain the + * command path and the certificate path. */ + global_ssl.passphrase_cmd[i + 2] = strdup(args[i + 2]); + if (!global_ssl.passphrase_cmd[i + 2]) { + memprintf(err, "'%s' : Could not allocate memory (command line)", args[0]); + goto err_alloc; + } + } + + return 0; + +err_alloc: + for (i = 0; i < arg_cnt; ++i) { + ha_free(&global_ssl.passphrase_cmd[i]); + } + ha_free(&global_ssl.passphrase_cmd); + + return ERR_ALERT | ERR_FATAL; +} + + /***************************** Bind keyword Parsing ********************************************/ /* for ca-file and ca-verify-file */ @@ -2715,6 +2774,8 @@ static struct cfg_kw_list cfg_kws = {ILH, { { CFG_LISTEN, "ssl-f-use", proxy_parse_ssl_f_use }, + { CFG_GLOBAL, "ssl-passphrase-cmd", ssl_parse_global_passphrase_cmd }, + { 0, NULL, NULL }, }}; diff --git a/src/ssl_ckch.c b/src/ssl_ckch.c index afd577d61..0427d0993 100644 --- a/src/ssl_ckch.c +++ b/src/ssl_ckch.c @@ -593,6 +593,7 @@ int ssl_sock_load_key_into_ckch(const char *path, char *buf, struct ckch_data *d BIO *in = NULL; int ret = 1; EVP_PKEY *key = NULL; + struct passphrase_cb_data cb_data = { path, 0 }; if (buf) { /* reading from a buffer */ @@ -613,7 +614,7 @@ int ssl_sock_load_key_into_ckch(const char *path, char *buf, struct ckch_data *d } /* Read Private Key */ - key = PEM_read_bio_PrivateKey(in, NULL, NULL, NULL); + key = PEM_read_bio_PrivateKey(in, NULL, ssl_sock_passwd_cb, &cb_data); if (key == NULL) { memprintf(err, "%sunable to load private key from file '%s'.\n", err && *err ? *err : "", path); diff --git a/src/ssl_sock.c b/src/ssl_sock.c index d64358641..00de17afe 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -150,6 +151,8 @@ struct global_ssl global_ssl = { .acme_scheduler = 1, #endif .renegotiate = SSL_RENEGOTIATE_DFLT, + .passphrase_cmd = NULL, + .passphrase_cmd_args_cnt = 0, }; @@ -3650,6 +3653,93 @@ out: return cfgerr; } +/* + * Certificate password callback. The password will be provided by the external + * program defined in global section (see 'ssl-passphrase-cmd'). It will be + * called in a separate fork and it should dump the password on standard output. + */ +int ssl_sock_passwd_cb(char *buf, int size, int rwflag, void *userdata) +{ + int pass_len; + int read_len; + pid_t pid = -1; + int wstatus = 0; + + int fd[2]; + + struct passphrase_cb_data *data = userdata; + + if (!data) + return -1; + + if (!global_ssl.passphrase_cmd) { + ha_alert("Trying to load a passphrase-protected private key without an 'ssl-passphrase-cmd' defined."); + return -1; + } + + /* From execvp manpage : "The first argument, by convention, should + * point to the filename associated with the file being executed." + * The second argument will be the certificate key path. + */ + ha_free(&global_ssl.passphrase_cmd[1]); + global_ssl.passphrase_cmd[1] = strdup(data->path); + + if (!global_ssl.passphrase_cmd[1]) { + ha_alert("ssl_sock_passwd_cb: allocation failure\n"); + return -1; + } + + if (pipe(fd) < 0) { + ha_alert("ssl_sock_passwd_cb: pipe error"); + return -1; + } + + pid = fork(); + + switch(pid) { + case -1: + ha_alert("ssl_sock_passwd_cb: could not fork"); + goto error; + case 0: + /* In child process, need to call external tool via execv to get + * passphrase */ + close(0); + dup2(fd[1], 1); + + execvp(global_ssl.passphrase_cmd[0], global_ssl.passphrase_cmd); + exit(1); + break; + default: + /* in parent */ + /* Close write side of pipe, it won't be used by the parent */ + close(fd[1]); + + while (1) { + read_len = read(fd[0], buf, size); + if (read_len <= 0) + break; + pass_len = read_len; + } + + /* Close read side of pipe */ + close(fd[0]); + waitpid(pid, &wstatus, 0); + if (WEXITSTATUS(wstatus) != 0) { + ha_alert("ssl_sock_passwd_cb: external tool error (%d)\n", WEXITSTATUS(wstatus)); + return -1; + } + } + + return pass_len; + +error: + close(fd[0]); + close(fd[1]); + return -1; + +} + + /* Create an initial CTX used to start the SSL connection before switchctx */ static int ssl_sock_initial_ctx(struct bind_conf *bind_conf) @@ -8001,6 +8091,14 @@ static void ssl_free_global(void) ha_free(&global_ssl.listen_default_client_sigalgs); ha_free(&global_ssl.connect_default_client_sigalgs); #endif + + if (global_ssl.passphrase_cmd) { + int i = 0; + for (; i < global_ssl.passphrase_cmd_args_cnt; ++i) { + ha_free(&global_ssl.passphrase_cmd[i]); + } + ha_free(&global_ssl.passphrase_cmd); + } } static void __ssl_sock_init(void) -- 2.47.3