public inbox for [email protected]
help / color / mirror / Atom feedFrom: Daniel Gustafsson <[email protected]>
To: Jacob Champion <[email protected]>
Cc: Michael Paquier <[email protected]>
Cc: Andres Freund <[email protected]>
Cc: Pgsql Hackers <[email protected]>
Subject: Re: Serverside SNI support in libpq
Date: Mon, 24 Nov 2025 15:53:31 +0100
Message-ID: <[email protected]> (raw)
In-Reply-To: <CAOYmi+m2Ks7D4obtXay3y-UNn6CkTNrmr_zWC25vKTdesatafA@mail.gmail.com>
References: <[email protected]>
<CAOYmi+nYV6Rr9BY4YfYyVdiQ5TzMZray6QPXwiO3pYSaow+-Tg@mail.gmail.com>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<CAOYmi+mSrV8hRaQkvGDf1Df4cmpv5SeTbTxppyxeonMe6MW8nA@mail.gmail.com>
<[email protected]>
<aa7gx3mychf3m2g67mbslzbxjy3if4enpcflstoa5pol3432x5@ugqz45gsvurq>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<CAOYmi+m2Ks7D4obtXay3y-UNn6CkTNrmr_zWC25vKTdesatafA@mail.gmail.com>
> On 12 Nov 2025, at 23:44, Jacob Champion <[email protected]> wrote:
> Did you have any thoughts on my earlier review [2]? The test patch
> attached there still fails on my machine with v9.
The attached incorporates your tests, fixes them to make them pass. The
culprit seemed to be a combination of a bug in the code (the verify callback
need to be defined in the default context even if there is no CA for it to be
called in an SNI setting because OpenSSL), and that the tests were matching
backend errors against frontend messages.
The other comments from your review are also addressed, as well as additional
cleanup and improved error handling.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v10-0001-Serverside-SNI-support-for-libpq.patch (60.5K, 2-v10-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 11984e057105c89d7f02f8f0be2a736ababee1f1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 24 Nov 2025 15:52:27 +0100
Subject: [PATCH v10] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 +++
doc/src/sgml/runtime.sgml | 67 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 204 ++++++++-
src/backend/libpq/be-secure-openssl.c | 430 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 15 +
src/backend/utils/misc/guc_tables.c | 8 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 237 ++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 +-
src/tools/pgindent/typedefs.list | 2 +
23 files changed, 1120 insertions(+), 62 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 023b3f03ba9..8bca363d542 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..2693dcb81ad 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the default fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..2d1691c7950 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..2d9baff92b5 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,193 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("SNI configuration not found in configuration file \"%s\"",
+ HostsFileName));
+ return NIL;
+ }
+
+ if (!ok)
+ return NIL;
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..38134ec87de 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,17 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +81,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +89,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
struct CallbackErr
{
@@ -102,11 +116,174 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (contexts != NIL)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ memset(&line, 0, sizeof(line));
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate"));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ hostcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list. Make sure to allocate the parsed rows in a temporary
+ * memory context such that we can avoid memory leaks.
+ */
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+ sni_hosts = load_hosts();
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"),
+ errhint("In strict ssl_snimode there need to be at least one entry in pg_hosts.conf."));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca && host->ssl_ca[0] != '\0')
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+
+ MemoryContextDelete(hostcxt);
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -132,10 +309,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -143,16 +327,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -161,19 +345,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -325,17 +509,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -348,16 +532,20 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
+ }
+
+ /*
+ * If we have a CA store, or SNI is enabled, always ask for SSL client
+ * cert, but don't fail if it's not presented. We might fail such
+ * connections later, depending on what we find in pg_hba.conf. The reason
+ * for enabling in the case of SNI even if there is no CA is that another
+ * context might have a CA, so the callback must be installed in order for
+ * that context.
+ */
+ if (ctx_ssl_ca_file[0] || ssl_snimode != SSL_SNIMODE_OFF)
SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
verify_cb);
- }
/*----------
* Load the Certificate Revocation List (CRL).
@@ -407,38 +595,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -771,6 +950,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1144,7 +1326,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1390,6 +1572,131 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Depending on the SNI mode we either
+ * require a perfect match, or we allow to fallback to a default configuration.
+ * Returning SSL_TLSEXT_ERR_ALERT_FATAL to OpenSSL will immediately terminate
+ * the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse. Throw an assertion to catch during testing
+ * but also ensure to terminate the connection in non-assert builds, even
+ * though this should never happen, just to be on the safe side.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF)
+ {
+ Assert(false);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch SSL context for SNI host"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * At this point we know that the requested hostname isn't configured in
+ * the pg_hosts file. In ssl_snimode "strict" it's an error if there was
+ * no match for the hostname in the TLS extension so terminate the
+ * connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1599,6 +1906,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1792,17 +2105,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1814,3 +2133,24 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..1431f92e332 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c6484aea087..f35a15b48df 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 1128167c025..186ac634dae 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1160,6 +1160,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
@@ -2735,6 +2742,14 @@
max => '0',
},
+{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets the SNI mode to use for the server.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_snimode',
+ boot_val => 'SSL_SNIMODE_DEFAULT',
+ options => 'ssl_snimode_options',
+},
+
{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.',
long_desc => 'An empty string means use the default cipher suites.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0209b2067a2..d429c658054 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -556,6 +563,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..4d0722e7bd7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..c953f24a58d 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2829,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e3748d3c8c9..c96818549cc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 5af005ad779..fe2d431291a 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -152,12 +153,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da89..8f08a38b789 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..a46ac325045 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches agsinst the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..90d7560ad11
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,237 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=strict",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv13 alert missing extension/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+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 wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+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" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after more reloads");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+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'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded");
+
+# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
+# time swapping out default contexts containing different CA configurations.
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+foreach my $default_ca ("", "root+client_ca", "root+server_ca")
+{
+ # The default CA should, not matter for the purposes of these tests, since
+ # we connect to the other hosts explicitly. Test with various default CA
+ # settings to ensure it's isolated from the actual connections.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
+ expected_stderr => qr/certificate verify failed/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
+ );
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 4159addb700..6ea5b64dc28 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n"
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57a8f0366a5..bd80880c453 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1206,6 +1206,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
view thread (58+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Serverside SNI support in libpq
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox