]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: ssl: Add certificate password callback that calls external command
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Tue, 28 Oct 2025 17:00:45 +0000 (18:00 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Wed, 29 Oct 2025 09:54:17 +0000 (10:54 +0100)
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
include/haproxy/ssl_sock-t.h
include/haproxy/ssl_sock.h
src/cfgparse-ssl.c
src/ssl_ckch.c
src/ssl_sock.c

index 136a7b749854f80c3fffacdf150bf66522864f42..1d53b4151a96832074e47d0a31597e63c129d52f 100644 (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
index d8c2613882a496b2aaa808e6f1e719986960c0e2..c076f7fb543d201aafb293a6b38d82f9993134ea 100644 (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 */
index 4658ab9c6486a86cb85fe69d03306d8e5371a030..d421061cac6a89331299e5726779951a997def2c 100644 (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);
index 29780978075d899eefdbfc7dc8886e0103ff505f..3b694def09a2d78b5088570e25a72e4d7fb9220e 100644 (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 },
 }};
 
index afd577d61cfc949486f96a30c1e7e1427d766471..0427d099374d2caacadbc1aa0ab454d0cbe33788 100644 (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);
index d643586415d8ee2ffdd291663d4d80c81da36dad..00de17afea1fe7c03275d977fca9824636d510f7 100644 (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)