From cdb2124b5fa66ec2d76446695175c15c5c43bd59 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Thu, 15 Oct 2026 17:36:54 +0000 Subject: [PATCH v1 2/3] ssl: Reject SNI hosts which cannot be loaded in EXEC_BACKEND children A pg_hosts.conf entry with a passphrase-protected key but without SSL_passphrase_cmd_reload was accepted at server start, even though EXEC_BACKEND children can never load it. Fix by setting up the passphrase callback as for a reload already at server start in EXEC_BACKEND builds, so that the server refuses to start instead. The previous commit is still needed, since children read pg_hosts.conf themselves and see changes made after server start even without a reload. Rework the test to cover that. Author: Zsolt Parragi --- doc/src/sgml/runtime.sgml | 10 +++--- src/backend/libpq/be-secure-openssl.c | 28 +++++++++++++++-- src/test/ssl/t/004_sni.pl | 44 +++++++++++++++++++-------- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 071e82a83bc..2c396427f59 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -2647,10 +2647,12 @@ openssl x509 -req -in server.csr -text -days 365 \ On Windows, all connections will perform a configuration reload due to the different process model of that platform, so SSL_passphrase_cmd_reload must be - set to on for every entry which has an - SSL_passphrase_cmd. If an entry fails to load - at backend start, SSL connections for the hosts of that entry will fail, - while the other entries remain usable. + set to on for every entry with a passphrase-protected + key, otherwise the server will refuse to start. Since every connection + reads pg_hosts.conf and the files it references + again, changes to them can take effect for new connections even without a + reload. If an entry fails to load at backend start, SSL connections for + the hosts of that entry will fail, while the other entries remain usable. diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index fbd4f1173bc..866de73005c 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -702,8 +702,17 @@ init_host_context(HostsLine *host, bool isServerStart, bool *hasWarned) /* * Set up the password callback, if configured. + * + * In EXEC_BACKEND builds every child process loads the configuration + * again, so even at server start we must set up the callback as for a + * reload. Otherwise a key which cannot be reloaded would be accepted + * here and then fail to load in every backend. */ +#ifdef EXEC_BACKEND + if (false) +#else if (isServerStart) +#endif { if (host->ssl_passphrase_cmd && host->ssl_passphrase_cmd[0]) { @@ -765,8 +774,23 @@ init_host_context(HostsLine *host, bool isServerStart, bool *hasWarned) host->ssl_key, SSL_FILETYPE_PEM) != 1) { - if (dummy_ssl_passwd_cb_called) - ereport(isServerStart ? FATAL : LOG, + if (dummy_ssl_passwd_cb_called && isServerStart) + { + /* Only reachable in EXEC_BACKEND builds, see above */ + ereport(FATAL, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", + host->ssl_key), + errdetail("On this platform, every backend process loads the SSL configuration again."), + errhint("Configure a passphrase command for this key and set \"%s\" to \"on\".", + host->sourcefile ? "SSL_passphrase_cmd_reload" : + "ssl_passphrase_command_supports_reload"), + host->sourcefile ? + errcontext("line %d of configuration file \"%s\"", + host->linenumber, host->sourcefile) : 0)); + } + else if (dummy_ssl_passwd_cb_called) + ereport(LOG, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", host->ssl_key))); diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl index 48ee6b455b6..14af06d3c38 100644 --- a/src/test/ssl/t/004_sni.pl +++ b/src/test/ssl/t/004_sni.pl @@ -459,22 +459,37 @@ $node->connect_ok( # Test reloading a passphrase protected key without reloading support in the # passphrase hook. Restarting should not give any errors in the log, but the -# subsequent reload should fail with an error regarding reloading. +# subsequent reload should fail with an error regarding reloading. On Windows +# and EXEC_BACKEND every backend reloads the configuration, so such a key is +# rejected already at server start. ok(unlink($node->data_dir . '/pg_hosts.conf')); $node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off' ); $node_loglocation = -s $node->logfile; $result = $node->restart(fail_ok => 1); -is($result, 1, - 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command' -); my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation); -unlike( - $log, - qr/cannot be reloaded because it requires a passphrase/, - 'log reload failure due to passphrase command reloading'); +if ($windows_os || $exec_backend =~ /on/) +{ + is($result, 0, + 'pg_hosts.conf: restart fails with password-protected key without passphrase command reloading' + ); + like( + $log, + qr/cannot be reloaded because it requires a passphrase/, + 'log startup failure due to passphrase command reloading'); +} +else +{ + is($result, 1, + 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command' + ); + unlike( + $log, + qr/cannot be reloaded because it requires a passphrase/, + 'log reload failure due to passphrase command reloading'); +} SKIP: { @@ -505,14 +520,16 @@ SKIP: } # A host whose key cannot be loaded in an EXEC_BACKEND child must only affect -# connections to that host. +# connections to that host. Such an entry is rejected at server start, but +# EXEC_BACKEND children read pg_hosts.conf themselves, so they see an entry +# added after server start even without a reload. ok(unlink($node->data_dir . '/pg_hosts.conf')); $node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-cn-only.key'); +$node->restart; $node->append_conf('pg_hosts.conf', 'example.org server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off' ); -$node->restart; $node_loglocation = -s $node->logfile; $node->connect_ok( @@ -530,9 +547,12 @@ if ($windows_os || $exec_backend =~ /on/) } else { - $node->connect_ok( + # Without EXEC_BACKEND the entry is not used until the configuration is + # reloaded. + $node->connect_fails( "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.org", - "pg_hosts.conf: connect to passphrase host without reload"); + "pg_hosts.conf: host added without reload is not used", + expected_stderr => qr/unrecognized name/); } # Configure with only non-SNI connections allowed -- 2.56.0