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).
This commit is contained in:
Remi Tricot-Le Breton 2025-10-28 18:00:45 +01:00 committed by William Lallemand
parent a011683622
commit 478dd7bad0
6 changed files with 181 additions and 1 deletions

View File

@ -3339,6 +3339,18 @@ ssl-dh-param-file <file>
"openssl dhparam <size>", where size should be at least 2048, as 1024-bit DH
parameters should not be considered secure anymore.
ssl-passphrase-cmd <cmd> <args> ...
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 <query>
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

View File

@ -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 */

View File

@ -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);

View File

@ -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 },
}};

View File

@ -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);

View File

@ -36,6 +36,7 @@
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netdb.h>
#include <netinet/tcp.h>
@ -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)