Serverside SNI support in libpq

Started by Daniel Gustafssonover 1 year ago42 messages
#1Daniel Gustafsson
daniel@yesql.se
1 attachment(s)

SNI was brought up the discussions around the ALPN work, and I have had asks
for it off-list, so I decided to dust off an old patch I started around the
time we got client-side SNI support but never finished (until now). Since
there is discussion and thinking around how we handle SSL right now I wanted to
share this early even though it will be parked in the July CF for now. There
are a few usecases for serverside SNI, allowing for completely disjoint CAs for
different hostnames is one that has come up. Using strict SNI mode (elaborated
on below) as a cross-host attack mitigation was mentioned in [0]/messages/by-id/e782e9f4-a0cd-49f5-800b-5e32a1b29183@eisentraut.org.

The attached patch adds serverside SNI support to libpq, it is still a bit
rough around the edges but I'm sharing it early to make sure I'm not designing
it in a direction that the community doesn't like. A new config file
$datadir/pg_hosts.conf is used for configuring which certicate and key should
be used for which hostname. The file is parsed in the same way as pg_ident
et.al so it allows for the usual include type statements we support. 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.

As of now the patch use default as the initial value for the GUC.

The way multiple certificates are handled is that libpq creates one SSL_CTX for
each at startup, and switch to the appropriate one when the connection is
inspected. Configuration handling is done in secure-common to not tie it to a
specific TLS backend (should we ever support more), but the validation of the
config values is left for the TLS backend.

There are a few known open items with this patch:

* There are two OpenSSL callbacks which can be used to inspect the hostname TLS
extension: SSL_CTX_set_tlsext_servername_callback and
SSL_CTX_set_client_hello_cb. The documentation for the latter says you
shouldn't use the former, and the docs for the former says you need it even if
you use the latter. For now I'm using SSL_CTX_set_tlsext_servername_callback
mainly because the OpenSSL tools themselves use that for SNI.

* The documentation is not polished at all and will require a more work to make
it passable I think. There are also lot's more testing that can be done, so
far it's pretty basic.

* I've so far only tested with OpenSSL and haven't yet verified how LibreSSL
handles this.

--
Daniel Gustafsson

[0]: /messages/by-id/e782e9f4-a0cd-49f5-800b-5e32a1b29183@eisentraut.org

Attachments:

v1-0001-POC-serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v1-0001-POC-serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From 7d36bcc4617f5c4ccfae50290a768b63fba796bb Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 26 Apr 2024 11:35:47 +0200
Subject: [PATCH v1] POC: 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.
---
 doc/src/sgml/config.sgml                      |  62 +++++
 doc/src/sgml/runtime.sgml                     |  40 +++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 156 +++++++++++
 src/backend/libpq/be-secure-openssl.c         | 253 +++++++++++++++---
 src/backend/libpq/be-secure.c                 |   8 +-
 src/backend/libpq/meson.build                 |   1 +
 src/backend/libpq/pg_hosts.conf.sample        |   5 +
 src/backend/utils/misc/guc.c                  |  26 ++
 src/backend/utils/misc/guc_tables.c           |  31 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/initdb/initdb.c                       |  16 +-
 src/include/libpq/hba.h                       |  14 +
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/libpq/libpq.h                     |   9 +
 src/include/utils/guc.h                       |   1 +
 src/test/ssl/meson.build                      |   1 +
 src/test/ssl/t/004_sni.pl                     |  86 ++++++
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 673 insertions(+), 41 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 e93208b2e6..6812173545 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1610,6 +1610,68 @@ 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.  Any connection specifying
+            <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+            a hostname which is missing in <filename>pg_hosts.conf</filename>
+            will be attempted using the default configuration. 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.
+           </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 6047b8171d..abe3a18fa5 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2425,6 +2425,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>
@@ -2552,6 +2558,40 @@ 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
+    SSL certificate, key and CA certificate to use for the connection.
+   </para>
+
+   <para>
+    SNI configuration is defined in the hosts configuration file, which is
+    named <filename>pg_hosts.conf</filename> and 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>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 againstt the hostname TLS extension in the SSL handshake.
+    <replaceable>SSL_certificate</replaceable>,
+    <replaceable>SSL_key</replaceable>, and
+    <replaceable>SSL_CA_certificate</replaceable>
+    are treated like
+    <xref linkend="guc-ssl-cert-file"/>,
+    <xref linkend="guc-ssl-key-file"/>, and
+    <xref linkend="guc-ssl-ca-file"/> respectively.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 6700aec039..788e9cd65e 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -180,6 +180,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 0582606192..957ed03e3b 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
 
 #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
@@ -171,3 +176,154 @@ 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);
+
+	/* Hostname */
+	field = list_head(tok_line->fields);
+	tokens = lfirst(field);
+	token = linitial(tokens);
+	parsedline->hostname = pstrdup(token->string);
+
+	/* SSL Certificate */
+	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 */
+	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 */
+	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);
+
+	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;
+	MemoryContext oldcxt;
+	MemoryContext hostcxt;
+
+	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);
+
+	hostcxt = AllocSetContextCreate(PostmasterContext,
+									"hosts file parser context",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostcxt);
+
+	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);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * 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("no SNI configuration added from configuration file  \"%s\"",
+					   HostsFileName));
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	if (!ok)
+	{
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 60cf68aac4..32af7c9f82 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,6 +51,13 @@
 #endif
 #include <openssl/x509v3.h>
 
+typedef struct HostContext
+{
+	const char *hostname;
+	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);
@@ -73,19 +80,23 @@ 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 *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 *Host_context = NULL;
 static bool SSL_initialized = false;
 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, const char *ctx_ssl_cert_file, const char *ctx_ssl_key_file, const char *ctx_ssl_ca_file);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -97,9 +108,8 @@ static const char *cert_errdetail;
 int
 be_tls_init(bool isServerStart)
 {
-	SSL_CTX    *context;
-	int			ssl_ver_min = -1;
-	int			ssl_ver_max = -1;
+	SSL_CTX    *ctx;
+	List	   *sni_hosts = NIL;
 
 	/* This stuff need be done only once. */
 	if (!SSL_initialized)
@@ -114,6 +124,123 @@ be_tls_init(bool isServerStart)
 		SSL_initialized = true;
 	}
 
+	/*
+	 * When ssl_snimode is off or default we load the certificate and key
+	 * specified in postgresql.conf and set that as the default host.
+	 */
+	if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+	{
+		HostContext *host_context;
+
+		ctx = ssl_init_context(isServerStart, ssl_cert_file, ssl_key_file, ssl_ca_file);
+		if (ctx == NULL)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not load default certificate")));
+			return -1;
+		}
+
+		host_context = palloc0(sizeof(HostContext));
+
+		host_context->hostname = pstrdup("*");
+		host_context->context = ctx;
+		host_context->default_host = true;
+
+		/*
+		 * Set flag to remember whether CA store has been loaded into
+		 * SSL_context.
+		 */
+		if (ssl_ca_file[0])
+			host_context->ssl_loaded_verify_locations = true;
+
+		/*
+		 * The contexts list is not used in ssl_snimode off but we add the
+		 * entry there anyways for consistency with the other modes.
+		 */
+		contexts = lappend(contexts, host_context);
+
+		/*
+		 * Install the default certificate which for ssl_snimode default can
+		 * be overridden in the callback if a hostname match is found.
+		 */
+		SSL_context = ctx;
+		Host_context = host_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;
+
+		/*
+		 * Load pg_hosts.conf and parse each row, returning the set of hosts
+		 * as a list.
+		 */
+		sni_hosts = load_hosts();
+
+		/*
+		 * In strict ssl_snimode there needs to be a working pg_hosts file,
+		 */
+		if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not load pg_hosts.conf file")));
+			return -1;
+		}
+
+		foreach(line, sni_hosts)
+		{
+			HostContext *host_context;
+			HostsLine  *host = lfirst(line);
+			SSL_CTX    *tmp;
+
+			tmp = ssl_init_context(isServerStart, host->ssl_cert, host->ssl_key, host->ssl_ca);
+			if (tmp != NULL)
+			{
+				SSL_context = tmp;
+
+				host_context = palloc(sizeof(HostContext));
+				host_context->hostname = pstrdup(host->hostname);
+				host_context->context = tmp;
+				host_context->default_host = false;
+
+				/*
+				 * Set flag to remember whether CA store has been loaded into
+				 * SSL_context.
+				 */
+				if (host->ssl_ca)
+					host_context->ssl_loaded_verify_locations = true;
+
+				contexts = lappend(contexts, host_context);
+			}
+		}
+	}
+
+	/* 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, const char *ctx_ssl_cert_file, const char *ctx_ssl_key_file, const char *ctx_ssl_ca_file)
+{
+	SSL_CTX    *context;
+	int			ssl_ver_min = -1;
+	int			ssl_ver_max = -1;
+
 	/*
 	 * 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
@@ -139,6 +266,13 @@ 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)
 	 */
@@ -150,16 +284,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;
 
 	/*
@@ -168,19 +302,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;
 	}
 
@@ -304,17 +438,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;
 		}
 
@@ -386,38 +520,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
@@ -1346,6 +1471,60 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+	const char *tlsext_hostname;
+	ListCell   *cell;
+	HostContext *host_context;
+
+	Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+	tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+	if (!tlsext_hostname)
+	{
+		if (ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("no hostname provided in callback")));
+			return SSL_TLSEXT_ERR_ALERT_FATAL;
+		}
+		else
+			return SSL_TLSEXT_ERR_OK;
+	}
+
+	foreach(cell, contexts)
+	{
+		host_context = lfirst(cell);
+
+		if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+		{
+			Host_context = host_context;
+			SSL_context = host_context->context;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			return SSL_TLSEXT_ERR_OK;
+		}
+	}
+
+	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 can return without doing anything since we
+	 * already installed the context for the default host when parsing the
+	 * hosts file.
+	 */
+	Assert(SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1539,6 +1718,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)
 {
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 1663f36b6b..dad1777f92 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -42,10 +42,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;
 
@@ -58,6 +54,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			*/
 /* ------------------------------------------------------------ */
@@ -97,7 +95,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 7c65314512..1c6269262c 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,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 0000000000..608210686e
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# 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 3fb6803998..4e1a5740d2 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,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"
@@ -1970,6 +1971,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
 	}
 	SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
 
+	/*
+	 * 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_tables.c b/src/backend/utils/misc/guc_tables.c
index ea2b0577bc..92e95ca9b1 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -483,6 +483,13 @@ static const struct config_enum_entry wal_compression_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
  */
@@ -540,6 +547,7 @@ char	   *cluster_name = "";
 char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
+char	   *HostsFileName;
 char	   *external_pid_file;
 
 char	   *application_name;
@@ -4476,6 +4484,17 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Sets the server's \"hosts\" configuration file."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&HostsFileName,
+		NULL,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
 			gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5096,6 +5115,18 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the SNI mode to use."),
+			NULL,
+			GUC_SUPERUSER_ONLY,
+		},
+		&ssl_snimode,
+		SSL_SNIMODE_DEFAULT,
+		ssl_snimode_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
 			gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 83d5df8e46..04a4b88a32 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -127,6 +127,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 30e17bd1d1..945d6cbac6 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,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;
@@ -1457,6 +1458,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);
@@ -2721,6 +2730,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");
@@ -2736,12 +2746,13 @@ 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\n"
+				"PG_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);
 	}
@@ -2749,6 +2760,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 8ea837ae82..2ee6e086bd 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,20 @@ typedef struct IdentLine
 	AuthToken  *pg_user;
 } IdentLine;
 
+typedef struct HostsLine
+{
+	int			linenumber;
+
+	char	   *sourcefile;
+	char	   *rawline;
+
+	bool		default_host;
+	char	   *hostname;
+	char	   *ssl_key;
+	char	   *ssl_cert;
+	char	   *ssl_ca;
+} 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 05cb1874c5..75ba89fcef 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -308,6 +308,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
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 83e338f604..a896b37adf 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -122,6 +122,7 @@ extern PGDLLIMPORT char *SSLECDHCurve;
 extern PGDLLIMPORT bool SSLPreferServerCiphers;
 extern PGDLLIMPORT int ssl_min_protocol_version;
 extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
 
 enum ssl_protocol_versions
 {
@@ -132,6 +133,13 @@ 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
  */
@@ -139,5 +147,6 @@ extern int	run_ssl_passphrase_command(const char *prompt, bool is_server_start,
 									   char *buf, int size);
 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 e4a594b5e8..1e420c3df3 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -276,6 +276,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/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..55f5887d9c 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 0000000000..d95af50376
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,86 @@
+
+# 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';
+}
+
+sub reset_pg_hosts
+{
+	my $node = shift;
+
+	ok(unlink($node->data_dir . '/pg_hosts.conf'));
+	$node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt");
+	$node->reload;
+	return;
+}
+
+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 =
+  "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->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require",
+	expected_stderr => qr/unrecognized name/);
+
+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->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2311f82d81..c4e9e88259 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1140,6 +1140,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#2Cary Huang
cary.huang@highgo.ca
In reply to: Daniel Gustafsson (#1)
Re: Serverside SNI support in libpq

The following review has been posted through the commitfest application:
make installcheck-world: not tested
Implements feature: not tested
Spec compliant: not tested
Documentation: not tested

This is an interesting feature on PostgreSQL server side where it can swap the
certificate settings based on the incoming hostnames in SNI field in client
hello message.

I think this patch resonate with a patch I shared awhile ago
( https://commitfest.postgresql.org/48/4924/ ) that adds multiple certificate
support on the libpq client side while this patch adds multiple certificate
support on the server side. My patch allows user to supply multiple certs, keys,
sslpasswords in comma separated format and the libpq client will pick one that
matches the CA issuer names sent by the server. In relation with your patch,
this CA issuer name would match the CA certificate configured in pg_hosts.cfg.

I had a look at the patch and here's my comments:

+   <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
+    SSL certificate, key and CA certificate to use for the connection.
+   </para>

pg_hosts should also have sslpassword_command just like in the postgresql.conf in
case the sslkey for a particular host is encrypted with a different password.

+	/*
+	 * 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);

If libpq client does not provide SNI, this callback will not be called, so there
is not a chance to check for a hostname match from pg_hosts, swap the TLS CONTEXT,
or possibly reject the connection even in strict mode. The TLS handshake in such
case shall proceed and server will use the certificate specified in
postgresql.conf (if these are loaded) to complete the handshake with the client.
There is a comment in the patch that reads:

- 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.

I am not sure if it implies that if ssl_snimode is strict, then the normal ssl_cert,
ssl_key and ca_cert…etc settings in postgresql.conf are ignored?

thank you

Cary Huang
-------------
HighGo Software Inc. (Canada)
cary.huang@highgo.ca
www.highgo.ca

#3Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#1)
Re: Serverside SNI support in libpq

On Fri, May 10, 2024 at 7:23 AM Daniel Gustafsson <daniel@yesql.se> wrote:

The way multiple certificates are handled is that libpq creates one SSL_CTX for
each at startup, and switch to the appropriate one when the connection is
inspected.

I fell in a rabbit hole while testing this patch, so this review isn't
complete, but I don't want to delay it any more. I see a few
possibly-related problems with the handling of SSL_context.

The first is that reloading the server configuration doesn't reset the
contexts list, so the server starts behaving in really strange ways
the longer you test. That's an easy enough fix, but things got weirder
when I did. Part of that weirdness is that SSL_context gets set to the
last initialized context, so fallback doesn't always behave in a
deterministic fashion. But we do have to set it to something, to
create the SSL object itself...

I tried patching all that, but I continue to see nondeterministic
behavior, including the wrong certificate chain occasionally being
served, and the servername callback being called twice for each
connection (?!).

Since I can't reproduce the weirdest bits under a debugger yet, I
don't really know what's happening. Maybe my patches are buggy. Or
maybe we're running into some chicken-and-egg madness? The order of
operations looks like this:

1. Create a list of contexts, selecting one as an arbitrary default
2. Create an SSL object from our default context
3. During the servername_callback, reparent that SSL object (which has
an active connection underway) to the actual context we want to use
4. Complete the connection

It's step 3 that I'm squinting at. I wondered how, exactly, that
worked in practice, and based on this issue the answer might be "not
well":

https://github.com/openssl/openssl/issues/6109

Matt Caswell appears to be convinced that SSL_set_SSL_CTX() is
fundamentally broken. So it might just be FUD, but I'm wondering if we
should instead be using the SSL_ flavors of the API to reassign the
certificate chain on the SSL pointer directly, inside the callback,
instead of trying to set them indirectly via the SSL_CTX_ API.

Have you seen any weird behavior like this on your end? I'm starting
to doubt my test setup... On the plus side, I now have a handful of
debugging patches for a future commitfest.

Thanks,
--Jacob

#4Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Cary Huang (#2)
Re: Serverside SNI support in libpq

On Fri, May 24, 2024 at 12:55 PM Cary Huang <cary.huang@highgo.ca> wrote:

pg_hosts should also have sslpassword_command just like in the postgresql.conf in
case the sslkey for a particular host is encrypted with a different password.

Good point. There is also the HBA-related handling of client
certificate settings (such as pg_ident)...

I really dislike that these things are governed by various different
files, but I also feel like I'm opening up a huge can of worms by
requesting nestable configurations.

+       if (ssl_snimode != SSL_SNIMODE_OFF)
+               SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);

If libpq client does not provide SNI, this callback will not be called, so there
is not a chance to check for a hostname match from pg_hosts, swap the TLS CONTEXT,
or possibly reject the connection even in strict mode.

I'm mistrustful of my own test setup (see previous email to the
thread), but I don't seem to be able to reproduce this. With sslsni=0
set, strict mode correctly shuts down the connection for me. Can you
share your setup?

(The behavior you describe might be a useful setting in practice, to
let DBAs roll out strict protection for new clients gracefully without
immediately blocking older ones.)

Thanks,
--Jacob

#5Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#3)
1 attachment(s)
Re: Serverside SNI support in libpq

On 25 Jul 2024, at 19:51, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

The attached rebased version adds proper list reset, a couple of bugfixes
around cert loading and the ability to set ssl_passhprase_command (and reload)
in the hosts file.

Matt Caswell appears to be convinced that SSL_set_SSL_CTX() is
fundamentally broken. So it might just be FUD, but I'm wondering if we
should instead be using the SSL_ flavors of the API to reassign the
certificate chain on the SSL pointer directly, inside the callback,
instead of trying to set them indirectly via the SSL_CTX_ API.

Maybe, but I would feel better about changing if I can could reproduce the
issues (see below).

Have you seen any weird behavior like this on your end? I'm starting
to doubt my test setup...

I've not been able to reproduce any behaviour like what you describe.

On the plus side, I now have a handful of
debugging patches for a future commitfest.

Do you have them handy for running tests on this version?

--
Daniel Gustafsson

Attachments:

v2-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v2-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From c8aea86957ad12b7e48a32370eb2c565c20a2205 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v2] 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.

Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  62 ++++
 doc/src/sgml/runtime.sgml                     |  50 +++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 201 ++++++++++-
 src/backend/libpq/be-secure-openssl.c         | 315 ++++++++++++++++--
 src/backend/libpq/be-secure.c                 |   8 +-
 src/backend/libpq/meson.build                 |   1 +
 src/backend/libpq/pg_hosts.conf.sample        |   5 +
 src/backend/utils/misc/guc.c                  |  26 ++
 src/backend/utils/misc/guc_tables.c           |  31 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/initdb/initdb.c                       |  16 +-
 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 +
 src/test/ssl/meson.build                      |   1 +
 src/test/ssl/t/004_sni.pl                     |  92 +++++
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 798 insertions(+), 48 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 e0c8325a39..1cecaccee7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ 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.  Any connection specifying
+            <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+            a hostname which is missing in <filename>pg_hosts.conf</filename>
+            will be attempted using the default configuration. 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.
+           </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 94135e9d5e..0ac79ae28d 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,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>
@@ -2571,6 +2577,50 @@ 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
+    SSL certificate, key and CA certificate to use for the connection.
+   </para>
+
+   <para>
+    SNI configuration is defined in the hosts configuration file, which is
+    named <filename>pg_hosts.conf</filename> and 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>
+  </sect2>
  </sect1>
 
  <sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 84302cc6da..bc8accbde0 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,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 0cb201acb1..7c900edd45 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
 
 #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
@@ -37,19 +42,20 @@
  * 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 +181,194 @@ 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)
+				parsedline->ssl_passphrase_reload = true;
+			else if (token->string[0] == '0'
+					 || pg_strcasecmp(token->string, "false") == 0
+					 || pg_strcasecmp(token->string, "off") == 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 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;
+	MemoryContext oldcxt;
+	MemoryContext hostcxt;
+
+	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);
+
+	hostcxt = AllocSetContextCreate(PostmasterContext,
+									"hosts file parser context",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostcxt);
+
+	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);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * 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("no SNI configuration added from configuration file  \"%s\"",
+					   HostsFileName));
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	if (!ok)
+	{
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 91a86d62a3..5acfdeb625 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
 #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;
+	bool		ssl_passphrase_support_reload;
+} 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 +82,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 +90,16 @@ 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 *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);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -96,11 +110,155 @@ static const char *cert_errdetail;
 
 int
 be_tls_init(bool isServerStart)
+{
+	SSL_CTX    *ctx;
+	List	   *sni_hosts = NIL;
+
+	/*
+	 * If there are contexts loaded when we init they should be released. This
+	 * should only be possible when reloading, but to keep any subtle bugs at
+	 * arms length we check unconditionally with an assert for non-production
+	 * builds.
+	 */
+	if (contexts != NIL)
+	{
+		Assert(isServerStart == false);
+		free_contexts();
+	}
+
+	/*
+	 * When ssl_snimode is off or default we load the certificate and key
+	 * specified in postgresql.conf and set that as the default host.
+	 */
+	if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+	{
+		HostContext *host_context;
+		HostsLine	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;
+		}
+
+		host_context = palloc0(sizeof(HostContext));
+
+		host_context->hostname = pstrdup("*");
+		host_context->context = ctx;
+		host_context->default_host = true;
+
+		/*
+		 * Set flag to remember whether CA store has been loaded into
+		 * SSL_context.
+		 */
+		if (ssl_ca_file[0])
+			host_context->ssl_loaded_verify_locations = true;
+
+		/*
+		 * The contexts list is not used in ssl_snimode off but we add the
+		 * entry there anyways for consistency with the other modes.
+		 */
+		contexts = lappend(contexts, host_context);
+
+		/*
+		 * Install the default certificate which for ssl_snimode default can
+		 * be overridden in the callback if a hostname match is found.
+		 */
+		SSL_context = ctx;
+		Host_context = host_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;
+
+		/*
+		 * Load pg_hosts.conf and parse each row, returning the set of hosts
+		 * as a list.
+		 */
+		sni_hosts = load_hosts();
+
+		/*
+		 * In strict ssl_snimode there needs to be a working pg_hosts file,
+		 */
+		if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load pg_hosts.conf file"));
+			return -1;
+		}
+
+		foreach(line, sni_hosts)
+		{
+			HostContext *host_context;
+			HostsLine  *host = lfirst(line);
+
+			SSL_context = ssl_init_context(isServerStart, host);
+			if (SSL_context == NULL)
+			{
+				ereport(isServerStart ? FATAL : LOG,
+						errcode(ERRCODE_CONFIG_FILE_ERROR),
+						errmsg("unable to load certificate from pg_hosts.conf file"));
+				return -1;
+			}
+
+			host_context = palloc(sizeof(HostContext));
+			host_context->hostname = pstrdup(host->hostname);
+			host_context->context = SSL_context;
+			host_context->default_host = false;
+			if (host->ssl_passphrase_cmd != NULL)
+				host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+			host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+			/*
+			 * Set flag to remember whether CA store has been loaded into
+			 * SSL_context.
+			 */
+			if (host->ssl_ca)
+				host_context->ssl_loaded_verify_locations = true;
+
+			contexts = lappend(contexts, host_context);
+		}
+	}
+
+	/* 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
@@ -126,10 +284,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;
@@ -137,16 +302,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;
 
 	/*
@@ -155,19 +320,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;
 	}
 
@@ -319,17 +484,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;
 		}
 
@@ -401,38 +566,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
@@ -1132,7 +1288,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);
 }
 
 /*
@@ -1369,6 +1525,60 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+	const char *tlsext_hostname;
+	ListCell   *cell;
+	HostContext *host_context;
+
+	Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+	tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+	if (!tlsext_hostname)
+	{
+		if (ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("no hostname provided in callback")));
+			return SSL_TLSEXT_ERR_ALERT_FATAL;
+		}
+		else
+			return SSL_TLSEXT_ERR_OK;
+	}
+
+	foreach(cell, contexts)
+	{
+		host_context = lfirst(cell);
+
+		if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+		{
+			Host_context = host_context;
+			SSL_context = host_context->context;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			return SSL_TLSEXT_ERR_OK;
+		}
+	}
+
+	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 can return without doing anything since we
+	 * already installed the context for the default host when parsing the
+	 * hosts file.
+	 */
+	Assert(SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1578,6 +1788,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)
 {
@@ -1771,17 +1987,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
 
 			/*
@@ -1793,3 +2015,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
 			SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
 	}
 }
+
+static void
+free_contexts(void)
+{
+	if (contexts == NIL)
+		return;
+
+	foreach_ptr(HostContext, host, contexts)
+	{
+		if (host->hostname)
+			pfree(unconstify(char *, host->hostname));
+		if (host->ssl_passphrase)
+			pfree(unconstify(char *, host->ssl_passphrase));
+		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 2139f81f24..ad3066b63c 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 7c65314512..1c6269262c 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,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 0000000000..608210686e
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# 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 c10c0844ab..b3e1cf0b25 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,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"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
 	}
 	SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
 
+	/*
+	 * 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_tables.c b/src/backend/utils/misc/guc_tables.c
index 8cf1afbad2..dca5a15f5c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_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
  */
@@ -538,6 +545,7 @@ char	   *cluster_name = "";
 char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
+char	   *HostsFileName;
 char	   *external_pid_file;
 
 char	   *application_name;
@@ -4562,6 +4570,17 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Sets the server's \"hosts\" configuration file."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&HostsFileName,
+		NULL,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
 			gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5204,6 +5223,18 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the SNI mode to use."),
+			NULL,
+			GUC_SUPERUSER_ONLY,
+		},
+		&ssl_snimode,
+		SSL_SNIMODE_DEFAULT,
+		ssl_snimode_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
 			gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a2ac7575ca..902c4eccef 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -120,6 +120,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 9a91830783..984763da08 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,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;
@@ -1512,6 +1513,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);
@@ -2771,6 +2780,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");
@@ -2786,12 +2796,13 @@ 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\n"
+				"PG_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);
 	}
@@ -2799,6 +2810,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 8ea837ae82..d1eb750368 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,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 9109b2c334..cf0e87a28c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -314,6 +314,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
@@ -326,7 +327,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 07e5b12536..98da8b61eb 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;
@@ -134,12 +135,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 840b0fe57f..4c33ba7ca4 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -284,6 +284,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/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..55f5887d9c 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 0000000000..d2efcd850c
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,92 @@
+
+# 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 =
+  "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");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require",
+	expected_stderr => qr/unexpected eof/);
+
+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->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
+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" no');
+my $result = $node->restart(fail_ok => 1);
+is($result, 0, 'restart fails with password-protected key when using the wrong passphrase command');
+
+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" no');
+$result = $node->restart(fail_ok => 1);
+is($result, 1, 'restart succeeds with password-protected key when using the correct passphrase command');
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2d4c870423..15ccf4cf46 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1158,6 +1158,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#6Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#5)
Re: Serverside SNI support in libpq

On Tue, Dec 3, 2024 at 5:58 AM Daniel Gustafsson <daniel@yesql.se> wrote:

Have you seen any weird behavior like this on your end? I'm starting
to doubt my test setup...

I've not been able to reproduce any behaviour like what you describe.

Hm, v2 is different enough that I'm going to need to check my notes
and try to reproduce again. At first glance, I am still seeing strange
reload behavior (e.g. issuing `pg_ctl reload` a couple of times in a
row leads to the server disappearing without any log messages
indicating why).

On the plus side, I now have a handful of
debugging patches for a future commitfest.

Do you have them handy for running tests on this version?

I'll work on cleaning them up. I'd meant to contribute them
individually by now, but I got a bit sidetracked...

Thanks!
--Jacob

#7Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#6)
1 attachment(s)
Re: Serverside SNI support in libpq

On 4 Dec 2024, at 01:43, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

On Tue, Dec 3, 2024 at 5:58 AM Daniel Gustafsson <daniel@yesql.se> wrote:

Have you seen any weird behavior like this on your end? I'm starting
to doubt my test setup...

I've not been able to reproduce any behaviour like what you describe.

Hm, v2 is different enough that I'm going to need to check my notes
and try to reproduce again. At first glance, I am still seeing strange
reload behavior (e.g. issuing `pg_ctl reload` a couple of times in a
row leads to the server disappearing without any log messages
indicating why).

On the plus side, I now have a handful of
debugging patches for a future commitfest.

Do you have them handy for running tests on this version?

I'll work on cleaning them up. I'd meant to contribute them
individually by now, but I got a bit sidetracked...

No worries, I know you have a big path on your plate right now. The attached
v3 fixes a small buglet in the tests and adds silly reload testing to try and
stress out any issues.

--
Daniel Gustafsson

Attachments:

v3-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v3-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From 2033491062b359cd62df4ec1560947ae21f0868c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v3] 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.

Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  62 ++++
 doc/src/sgml/runtime.sgml                     |  50 +++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 203 ++++++++++-
 src/backend/libpq/be-secure-openssl.c         | 315 ++++++++++++++++--
 src/backend/libpq/be-secure.c                 |   8 +-
 src/backend/libpq/meson.build                 |   1 +
 src/backend/libpq/pg_hosts.conf.sample        |   5 +
 src/backend/utils/misc/guc.c                  |  26 ++
 src/backend/utils/misc/guc_tables.c           |  31 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/bin/initdb/initdb.c                       |  16 +-
 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 +
 src/test/ssl/meson.build                      |   1 +
 src/test/ssl/t/004_sni.pl                     | 135 ++++++++
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 843 insertions(+), 48 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 e0c8325a39..1cecaccee7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ 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.  Any connection specifying
+            <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+            a hostname which is missing in <filename>pg_hosts.conf</filename>
+            will be attempted using the default configuration. 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.
+           </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 94135e9d5e..0ac79ae28d 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,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>
@@ -2571,6 +2577,50 @@ 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
+    SSL certificate, key and CA certificate to use for the connection.
+   </para>
+
+   <para>
+    SNI configuration is defined in the hosts configuration file, which is
+    named <filename>pg_hosts.conf</filename> and 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>
+  </sect2>
  </sect1>
 
  <sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 84302cc6da..bc8accbde0 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,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 0cb201acb1..e483ca7944 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
 
 #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
@@ -37,19 +42,20 @@
  * 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 +181,196 @@ 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 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;
+	MemoryContext oldcxt;
+	MemoryContext hostcxt;
+
+	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);
+
+	hostcxt = AllocSetContextCreate(PostmasterContext,
+									"hosts file parser context",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostcxt);
+
+	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);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * 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("no SNI configuration added from configuration file  \"%s\"",
+					   HostsFileName));
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	if (!ok)
+	{
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 91a86d62a3..5acfdeb625 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
 #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;
+	bool		ssl_passphrase_support_reload;
+} 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 +82,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 +90,16 @@ 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 *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);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -96,11 +110,155 @@ static const char *cert_errdetail;
 
 int
 be_tls_init(bool isServerStart)
+{
+	SSL_CTX    *ctx;
+	List	   *sni_hosts = NIL;
+
+	/*
+	 * If there are contexts loaded when we init they should be released. This
+	 * should only be possible when reloading, but to keep any subtle bugs at
+	 * arms length we check unconditionally with an assert for non-production
+	 * builds.
+	 */
+	if (contexts != NIL)
+	{
+		Assert(isServerStart == false);
+		free_contexts();
+	}
+
+	/*
+	 * When ssl_snimode is off or default we load the certificate and key
+	 * specified in postgresql.conf and set that as the default host.
+	 */
+	if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+	{
+		HostContext *host_context;
+		HostsLine	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;
+		}
+
+		host_context = palloc0(sizeof(HostContext));
+
+		host_context->hostname = pstrdup("*");
+		host_context->context = ctx;
+		host_context->default_host = true;
+
+		/*
+		 * Set flag to remember whether CA store has been loaded into
+		 * SSL_context.
+		 */
+		if (ssl_ca_file[0])
+			host_context->ssl_loaded_verify_locations = true;
+
+		/*
+		 * The contexts list is not used in ssl_snimode off but we add the
+		 * entry there anyways for consistency with the other modes.
+		 */
+		contexts = lappend(contexts, host_context);
+
+		/*
+		 * Install the default certificate which for ssl_snimode default can
+		 * be overridden in the callback if a hostname match is found.
+		 */
+		SSL_context = ctx;
+		Host_context = host_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;
+
+		/*
+		 * Load pg_hosts.conf and parse each row, returning the set of hosts
+		 * as a list.
+		 */
+		sni_hosts = load_hosts();
+
+		/*
+		 * In strict ssl_snimode there needs to be a working pg_hosts file,
+		 */
+		if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load pg_hosts.conf file"));
+			return -1;
+		}
+
+		foreach(line, sni_hosts)
+		{
+			HostContext *host_context;
+			HostsLine  *host = lfirst(line);
+
+			SSL_context = ssl_init_context(isServerStart, host);
+			if (SSL_context == NULL)
+			{
+				ereport(isServerStart ? FATAL : LOG,
+						errcode(ERRCODE_CONFIG_FILE_ERROR),
+						errmsg("unable to load certificate from pg_hosts.conf file"));
+				return -1;
+			}
+
+			host_context = palloc(sizeof(HostContext));
+			host_context->hostname = pstrdup(host->hostname);
+			host_context->context = SSL_context;
+			host_context->default_host = false;
+			if (host->ssl_passphrase_cmd != NULL)
+				host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+			host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+			/*
+			 * Set flag to remember whether CA store has been loaded into
+			 * SSL_context.
+			 */
+			if (host->ssl_ca)
+				host_context->ssl_loaded_verify_locations = true;
+
+			contexts = lappend(contexts, host_context);
+		}
+	}
+
+	/* 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
@@ -126,10 +284,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;
@@ -137,16 +302,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;
 
 	/*
@@ -155,19 +320,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;
 	}
 
@@ -319,17 +484,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;
 		}
 
@@ -401,38 +566,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
@@ -1132,7 +1288,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);
 }
 
 /*
@@ -1369,6 +1525,60 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+	const char *tlsext_hostname;
+	ListCell   *cell;
+	HostContext *host_context;
+
+	Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+	tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+	if (!tlsext_hostname)
+	{
+		if (ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("no hostname provided in callback")));
+			return SSL_TLSEXT_ERR_ALERT_FATAL;
+		}
+		else
+			return SSL_TLSEXT_ERR_OK;
+	}
+
+	foreach(cell, contexts)
+	{
+		host_context = lfirst(cell);
+
+		if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+		{
+			Host_context = host_context;
+			SSL_context = host_context->context;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			return SSL_TLSEXT_ERR_OK;
+		}
+	}
+
+	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 can return without doing anything since we
+	 * already installed the context for the default host when parsing the
+	 * hosts file.
+	 */
+	Assert(SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1578,6 +1788,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)
 {
@@ -1771,17 +1987,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
 
 			/*
@@ -1793,3 +2015,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
 			SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
 	}
 }
+
+static void
+free_contexts(void)
+{
+	if (contexts == NIL)
+		return;
+
+	foreach_ptr(HostContext, host, contexts)
+	{
+		if (host->hostname)
+			pfree(unconstify(char *, host->hostname));
+		if (host->ssl_passphrase)
+			pfree(unconstify(char *, host->ssl_passphrase));
+		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 2139f81f24..ad3066b63c 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 7c65314512..1c6269262c 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,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 0000000000..608210686e
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# 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 c10c0844ab..b3e1cf0b25 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,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"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
 	}
 	SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
 
+	/*
+	 * 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_tables.c b/src/backend/utils/misc/guc_tables.c
index 8cf1afbad2..dca5a15f5c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_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
  */
@@ -538,6 +545,7 @@ char	   *cluster_name = "";
 char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
+char	   *HostsFileName;
 char	   *external_pid_file;
 
 char	   *application_name;
@@ -4562,6 +4570,17 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Sets the server's \"hosts\" configuration file."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&HostsFileName,
+		NULL,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
 			gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5204,6 +5223,18 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the SNI mode to use."),
+			NULL,
+			GUC_SUPERUSER_ONLY,
+		},
+		&ssl_snimode,
+		SSL_SNIMODE_DEFAULT,
+		ssl_snimode_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
 			gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a2ac7575ca..902c4eccef 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -120,6 +120,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 9a91830783..984763da08 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,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;
@@ -1512,6 +1513,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);
@@ -2771,6 +2780,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");
@@ -2786,12 +2796,13 @@ 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\n"
+				"PG_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);
 	}
@@ -2799,6 +2810,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 8ea837ae82..d1eb750368 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,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 9109b2c334..cf0e87a28c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -314,6 +314,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
@@ -326,7 +327,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 07e5b12536..98da8b61eb 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;
@@ -134,12 +135,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 840b0fe57f..4c33ba7ca4 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -284,6 +284,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/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..55f5887d9c 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 0000000000..1e7e397080
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,135 @@
+
+# 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 =
+  "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");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require",
+	expected_stderr => qr/unexpected eof/);
+
+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->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
+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');
+
+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');
+
+$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_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"1 connect with correct server CA cert file sslmode=require");
+
+# Stress testing during patch debugging and review, unlikely to be merged in
+# this state.
+for (my $i = 0; $i < 100; $i++)
+{
+	if (int(rand(10)) < 3)
+	{
+		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');
+	}
+	$node->append_conf('pg_hosts.conf', 'localhost_' . $i . ' server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+	$node->reload;
+	$node->reload;
+	$node->connect_ok(
+		"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+		"connect with correct server CA cert file sslmode=require during reload loop");
+}
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require after reload loop");
+
+# 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');
+$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",
+	expected_stderr => qr/unexpected eof/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2d4c870423..15ccf4cf46 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1158,6 +1158,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#8Michael Paquier
michael@paquier.xyz
In reply to: Daniel Gustafsson (#7)
Re: Serverside SNI support in libpq

On Wed, Dec 04, 2024 at 02:44:18PM +0100, Daniel Gustafsson wrote:

No worries, I know you have a big path on your plate right now. The attached
v3 fixes a small buglet in the tests and adds silly reload testing to try and
stress out any issues.

Looks like this still fails quite heavily in the CI.. You may want to
look at that.
--
Michael

#9Daniel Gustafsson
daniel@yesql.se
In reply to: Michael Paquier (#8)
Re: Serverside SNI support in libpq

On 11 Dec 2024, at 01:34, Michael Paquier <michael@paquier.xyz> wrote:

On Wed, Dec 04, 2024 at 02:44:18PM +0100, Daniel Gustafsson wrote:

No worries, I know you have a big path on your plate right now. The attached
v3 fixes a small buglet in the tests and adds silly reload testing to try and
stress out any issues.

Looks like this still fails quite heavily in the CI.. You may want to
look at that.

Interestingly enough the CFBot hasn't picked up that there are new version
posted and the buildfailure is from the initial patch in the thread, which no
longer applies (as the CFBot righly points out). I'll try posting another
version later today to see if that gets it unstuck.

--
Daniel Gustafsson

#10Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#9)
1 attachment(s)
Re: Serverside SNI support in libpq

Attached is a rebase which fixes a few smaller things (and a pgperltidy run);
and adds a paragraph to the docs about how HBA clientname settings can't be
made per certificate set in an SNI config. As discussed with Jacob offlist,
there might be a case for supporting that but it will be a niche usecase within
a niche feature, so rather than complicating the code for something which might
never be used, it's likely better to document it and await feedback.

Are there any blockers for getting this in?

--
Daniel Gustafsson

Attachments:

v5-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v5-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From ed0a6b24a686b85077643dc8d3617957782eac11 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v5] 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.

Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  62 ++++
 doc/src/sgml/runtime.sgml                     |  57 ++++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 203 +++++++++++-
 src/backend/libpq/be-secure-openssl.c         | 307 +++++++++++++++---
 src/backend/libpq/be-secure.c                 |   8 +-
 src/backend/libpq/meson.build                 |   1 +
 src/backend/libpq/pg_hosts.conf.sample        |   5 +
 src/backend/utils/misc/guc.c                  |  26 ++
 src/backend/utils/misc/guc_tables.c           |  31 ++
 src/backend/utils/misc/postgresql.conf.sample |   3 +
 src/bin/initdb/initdb.c                       |  16 +-
 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/ssl/meson.build                      |   1 +
 src/test/ssl/t/004_sni.pl                     | 128 ++++++++
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 839 insertions(+), 50 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 9eedcf6f0f4..a10a1a24890 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ 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.
+           </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 59f39e89924..57dfe972100 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,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>
@@ -2571,6 +2577,57 @@ 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>
+     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 42d4a28e5aa..96adabf9581 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,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..67a50c7b24c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
 
 #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
@@ -37,19 +42,20 @@
  * 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 +181,196 @@ 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 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;
+	MemoryContext oldcxt;
+	MemoryContext hostcxt;
+
+	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);
+
+	hostcxt = AllocSetContextCreate(PostmasterContext,
+									"hosts file parser context",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostcxt);
+
+	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);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * 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("no SNI configuration added from configuration file  \"%s\"",
+					   HostsFileName));
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	if (!ok)
+	{
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 64ff3ce3d6a..bb3e724a190 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
 #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;
+	bool		ssl_passphrase_support_reload;
+} 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 +82,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 +90,16 @@ 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 *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);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -96,11 +110,147 @@ static const char *cert_errdetail;
 
 int
 be_tls_init(bool isServerStart)
+{
+	SSL_CTX    *ctx;
+	List	   *sni_hosts = NIL;
+
+	/*
+	 * If there are contexts loaded when we init they should be released.
+	 */
+	if (contexts != NIL)
+		free_contexts();
+
+	/*
+	 * When ssl_snimode is off or default we load the SSL configuration
+	 * specified in postgresql.conf and set that as the default host.
+	 */
+	if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+	{
+		HostContext *host_context;
+		HostsLine	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;
+		}
+
+		host_context = palloc0(sizeof(HostContext));
+
+		host_context->hostname = pstrdup("*");
+		host_context->context = ctx;
+		host_context->default_host = true;
+
+		/*
+		 * Set flag to remember whether CA store has been loaded into
+		 * SSL_context.
+		 */
+		if (ssl_ca_file[0])
+			host_context->ssl_loaded_verify_locations = true;
+
+		/*
+		 * The contexts list is not used in ssl_snimode off but we add the
+		 * entry there anyways for consistency with the other modes.
+		 */
+		contexts = lappend(contexts, host_context);
+
+		/*
+		 * Install the default certificate which for ssl_snimode default can
+		 * be overridden in the callback if a hostname match is found.
+		 */
+		SSL_context = ctx;
+		Host_context = host_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;
+
+		/*
+		 * Load pg_hosts.conf and parse each row, returning the set of hosts
+		 * as a list.
+		 */
+		sni_hosts = load_hosts();
+
+		/* In strict ssl_snimode there needs to be a working pg_hosts file */
+		if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load %s", "pg_hosts.conf"));
+			return -1;
+		}
+
+		foreach(line, sni_hosts)
+		{
+			HostContext *host_context;
+			HostsLine  *host = lfirst(line);
+
+			SSL_context = ssl_init_context(isServerStart, host);
+			if (SSL_context == NULL)
+			{
+				ereport(isServerStart ? FATAL : LOG,
+						errcode(ERRCODE_CONFIG_FILE_ERROR),
+						errmsg("unable to load certificate from pg_hosts.conf file"));
+				return -1;
+			}
+
+			host_context = palloc0(sizeof(HostContext));
+			host_context->hostname = pstrdup(host->hostname);
+			host_context->context = SSL_context;
+			host_context->default_host = false;
+			if (host->ssl_passphrase_cmd != NULL)
+				host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+			host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+			/*
+			 * Set flag to remember whether CA store has been loaded into this
+			 * SSL_context.
+			 */
+			if (host->ssl_ca)
+				host_context->ssl_loaded_verify_locations = true;
+
+			contexts = lappend(contexts, host_context);
+		}
+	}
+
+	/* 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
@@ -126,10 +276,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;
@@ -137,16 +294,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;
 
 	/*
@@ -155,19 +312,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;
 	}
 
@@ -319,17 +476,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;
 		}
 
@@ -401,38 +558,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
@@ -1132,7 +1280,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);
 }
 
 /*
@@ -1369,6 +1517,60 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+	const char *tlsext_hostname;
+	ListCell   *cell;
+	HostContext *host_context;
+
+	Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+	tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+	if (!tlsext_hostname)
+	{
+		if (ssl_snimode == SSL_SNIMODE_STRICT)
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("no hostname provided in callback")));
+			return SSL_TLSEXT_ERR_ALERT_FATAL;
+		}
+		else
+			return SSL_TLSEXT_ERR_OK;
+	}
+
+	foreach(cell, contexts)
+	{
+		host_context = lfirst(cell);
+
+		if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+		{
+			Host_context = host_context;
+			SSL_context = host_context->context;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			return SSL_TLSEXT_ERR_OK;
+		}
+	}
+
+	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 can return without doing anything since we
+	 * already installed the context for the default host when parsing the
+	 * hosts file.
+	 */
+	Assert(SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1578,6 +1780,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)
 {
@@ -1771,17 +1979,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
 
 			/*
@@ -1793,3 +2007,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
 			SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
 	}
 }
+
+static void
+free_contexts(void)
+{
+	if (contexts == NIL)
+		return;
+
+	foreach_ptr(HostContext, host, contexts)
+	{
+		if (host->hostname)
+			pfree(unconstify(char *, host->hostname));
+		if (host->ssl_passphrase)
+			pfree(unconstify(char *, host->ssl_passphrase));
+		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 91576f94285..b10e8f995ac 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 0f0421037e4..aed5ec16af2 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,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..608210686e1
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# 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 12192445218..1a3e5011b35 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,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"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
 	}
 	SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
 
+	/*
+	 * 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_tables.c b/src/backend/utils/misc/guc_tables.c
index 3cde94a1759..002e12fe1e4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_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
  */
@@ -538,6 +545,7 @@ char	   *cluster_name = "";
 char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
+char	   *HostsFileName;
 char	   *external_pid_file;
 
 char	   *application_name;
@@ -4622,6 +4630,17 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Sets the server's \"hosts\" configuration file."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&HostsFileName,
+		NULL,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
 			gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5264,6 +5283,18 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the SNI mode to use."),
+			NULL,
+			GUC_SUPERUSER_ONLY,
+		},
+		&ssl_snimode,
+		SSL_SNIMODE_DEFAULT,
+		ssl_snimode_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
 			gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 415f253096c..dec30a53a35 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
@@ -120,6 +122,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 21a0fe3ecd9..a6e680f7b44 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,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;
@@ -1542,6 +1543,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);
@@ -2805,6 +2814,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");
@@ -2820,12 +2830,13 @@ 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\n"
+				"PG_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);
 	}
@@ -2833,6 +2844,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 b20d0051f7d..a1ea3cf3e8c 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,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 7fe92b15477..a5f07aff046 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -323,6 +323,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
@@ -335,7 +336,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 8defcb6de19..6d9332cbe22 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;
@@ -134,12 +135,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 1233e07d7da..37cb3ecb5ae 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,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/ssl/meson.build b/src/test/ssl/meson.build
index cf8b2b9303a..7a2a5b8ca8c 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..0542c59ebcb
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,128 @@
+
+# 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 =
+  "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");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	"localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require",
+	expected_stderr => qr/unexpected eof/);
+
+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->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
+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'
+);
+
+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'
+);
+
+$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_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"1 connect with correct server CA cert file sslmode=require");
+
+# 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'
+);
+$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",
+	expected_stderr => qr/unexpected eof/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index fb39c915d76..4410bb24d53 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1165,6 +1165,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#11Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#10)
1 attachment(s)
Re: Serverside SNI support in libpq

On Wed, Feb 19, 2025 at 3:13 PM Daniel Gustafsson <daniel@yesql.se> wrote:

Are there any blockers for getting this in?

+ SSL_context = ssl_init_context(isServerStart, host);

I'm still not quite following the rationale behind the SSL_context
assignment. To maybe illustrate, attached are some tests that I
expected to pass, but don't.

After adding an additional host and reloading the config, the behavior
of the original fallback host seems to change. Am I misunderstanding
the designed fallback behavior, have I misdesigned my test, or is this
a bug?

Thanks,
--Jacob

Attachments:

tests.diff.txttext/plain; charset=US-ASCII; name=tests.diff.txtDownload
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index 0542c59ebcb..e183a953ede 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -57,6 +57,32 @@ $node->connect_ok(
 	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
 	"connect with correct server CA cert file sslmode=require");
 
+# This is added only for comparison with the same test case below.
+$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");
+
+# Why does this test fail?
+$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.crt server.key root.crt");
#12Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#11)
1 attachment(s)
Re: Serverside SNI support in libpq

On 24 Feb 2025, at 22:51, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

On Wed, Feb 19, 2025 at 3:13 PM Daniel Gustafsson <daniel@yesql.se> wrote:

Are there any blockers for getting this in?

+ SSL_context = ssl_init_context(isServerStart, host);

I'm still not quite following the rationale behind the SSL_context
assignment. To maybe illustrate, attached are some tests that I
expected to pass, but don't.

After adding an additional host and reloading the config, the behavior
of the original fallback host seems to change. Am I misunderstanding
the designed fallback behavior, have I misdesigned my test, or is this
a bug?

Thanks for the tests, they did in fact uncover a bug in how fallback was
handled which is now fixed. In doing so I revamped how the default context
handling is done, it now always use the GUCs in postgresql.conf for
consistency. The attached v6 rebase contains this as well as your tests as
well as general cleanup and comment writing etc.

--
Daniel Gustafsson

Attachments:

v6-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v6-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From ee5508fa4a2b114a6493d60d923b6586250713c5 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Thu, 27 Feb 2025 14:03:31 +0100
Subject: [PATCH v6] 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.

Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  66 ++++
 doc/src/sgml/runtime.sgml                     |  67 ++++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 203 +++++++++-
 src/backend/libpq/be-secure-openssl.c         | 356 ++++++++++++++++--
 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                  |  26 ++
 src/backend/utils/misc/guc_tables.c           |  31 ++
 src/backend/utils/misc/postgresql.conf.sample |   3 +
 src/bin/initdb/initdb.c                       |  16 +-
 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/ssl/meson.build                      |   1 +
 src/test/ssl/t/004_sni.pl                     | 164 ++++++++
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 937 insertions(+), 50 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 e55700f35b8..61f3178df82 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1678,6 +1678,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 59f39e89924..1e8f06ba2ce 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,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>
@@ -2571,6 +2577,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 defualt 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 42d4a28e5aa..96adabf9581 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,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..67a50c7b24c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
 
 #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
@@ -37,19 +42,20 @@
  * 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 +181,196 @@ 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 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;
+	MemoryContext oldcxt;
+	MemoryContext hostcxt;
+
+	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);
+
+	hostcxt = AllocSetContextCreate(PostmasterContext,
+									"hosts file parser context",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostcxt);
+
+	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);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * 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("no SNI configuration added from configuration file  \"%s\"",
+					   HostsFileName));
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	if (!ok)
+	{
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 64ff3ce3d6a..29544efa667 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
 #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;
+	bool		ssl_passphrase_support_reload;
+} 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 +82,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 +90,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);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -96,11 +111,160 @@ static const char *cert_errdetail;
 
 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.
+	 */
+	if (contexts != NIL)
+	{
+		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.
+	 */
+	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;
+
+		/*
+		 * Load pg_hosts.conf and parse each row, returning the set of hosts
+		 * as a list.
+		 */
+		sni_hosts = load_hosts();
+
+		/*
+		 * 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)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load %s", "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)
+			{
+				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.  The passphrase command fields are
+			 * however optional so we need to check whether those were set.
+			 */
+			host_context = palloc0(sizeof(HostContext));
+			host_context->hostname = pstrdup(host->hostname);
+			host_context->context = tmp_context;
+			host_context->default_host = false;
+			if (host->ssl_passphrase_cmd != NULL)
+				host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+			host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+			/*
+			 * Set flag to remember whether CA store has been loaded into this
+			 * SSL_context.
+			 */
+			if (host->ssl_ca)
+				host_context->ssl_loaded_verify_locations = true;
+
+			contexts = lappend(contexts, host_context);
+		}
+	}
+
+	/* 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
@@ -126,10 +290,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;
@@ -137,16 +308,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;
 
 	/*
@@ -155,19 +326,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;
 	}
 
@@ -319,17 +490,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;
 		}
 
@@ -401,38 +572,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
@@ -759,6 +921,9 @@ be_tls_close(Port *port)
 		pfree(port->peer_dn);
 		port->peer_dn = NULL;
 	}
+
+	Host_context = NULL;
+	SSL_context = NULL;
 }
 
 ssize_t
@@ -1132,7 +1297,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);
 }
 
 /*
@@ -1369,6 +1534,88 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+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.
+	 */
+	Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+	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)
+		{
+			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;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			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;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			return SSL_TLSEXT_ERR_OK;
+		}
+	}
+
+	/*
+	 * In ssl_snimode "strict" it's an error if there was no match for the
+	 * hostname in the TLS extension.  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;
+	SSL_set_SSL_CTX(ssl, SSL_context);
+	Assert(SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1578,6 +1825,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)
 {
@@ -1771,17 +2024,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
 
 			/*
@@ -1793,3 +2052,26 @@ 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));
+		if (host->ssl_passphrase)
+			pfree(unconstify(char *, host->ssl_passphrase));
+		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 91576f94285..b10e8f995ac 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 12192445218..1a3e5011b35 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,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"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
 	}
 	SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
 
+	/*
+	 * 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_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39c5..b7e868b128e 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -476,6 +476,13 @@ static const struct config_enum_entry wal_compression_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
  */
@@ -540,6 +547,7 @@ char	   *cluster_name = "";
 char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
+char	   *HostsFileName;
 char	   *external_pid_file;
 
 char	   *application_name;
@@ -4624,6 +4632,17 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Sets the server's \"hosts\" configuration file."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&HostsFileName,
+		NULL,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
 			gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5277,6 +5296,18 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the SNI mode to use."),
+			NULL,
+			GUC_SUPERUSER_ONLY,
+		},
+		&ssl_snimode,
+		SSL_SNIMODE_DEFAULT,
+		ssl_snimode_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
 			gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5362ff80519..a20a5ba5d75 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
@@ -120,6 +122,7 @@
 #ssl_dh_params_file = ''
 #ssl_passphrase_command = ''
 #ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
 
 # OAuth
 #oauth_validator_libraries = ''	# comma-separated list of trusted validator modules
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 21a0fe3ecd9..a6e680f7b44 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,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;
@@ -1542,6 +1543,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);
@@ -2805,6 +2814,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");
@@ -2820,12 +2830,13 @@ 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\n"
+				"PG_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);
 	}
@@ -2833,6 +2844,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 3657f182db3..3d8e33533b8 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 7fe92b15477..a5f07aff046 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -323,6 +323,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
@@ -335,7 +336,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 aeb66ca40cf..5feed0eb0a4 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;
@@ -134,12 +135,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 1233e07d7da..37cb3ecb5ae 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,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/ssl/meson.build b/src/test/ssl/meson.build
index cf8b2b9303a..7a2a5b8ca8c 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..f0ce048273a
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,164 @@
+
+# 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=struct",
+	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/tlsv1 unrecognized name/);
+
+# 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",
+	"1 connect with correct server CA cert file sslmode=require");
+
+# 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'
+);
+$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",
+	expected_stderr => qr/tlsv1 unrecognized name/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index cfbab589d61..96c91831feb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1168,6 +1168,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#13Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#12)
1 attachment(s)
Re: Serverside SNI support in libpq

On Thu, Feb 27, 2025 at 5:38 AM Daniel Gustafsson <daniel@yesql.se> wrote:

Thanks for the tests, they did in fact uncover a bug in how fallback was
handled which is now fixed. In doing so I revamped how the default context
handling is done, it now always use the GUCs in postgresql.conf for
consistency. The attached v6 rebase contains this as well as your tests as
well as general cleanup and comment writing etc.

Great, thanks!

Revisiting my concerns from upthread:

On Thu, Jul 25, 2024 at 10:51 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I tried patching all that, but I continue to see nondeterministic
behavior, including the wrong certificate chain occasionally being
served, and the servername callback being called twice for each
connection (?!).

1) The wrong chain being served was due to the fallback bug, now fixed.
2) The servername callback happening twice is due to the TLS 1.3
HelloRetryRequest problem with our ssl_groups (which reminded me to
ping that thread [1]/messages/by-id/CAOYmi+nTwu7=aUGCkf6L-ULqS8itNP7uc9nUmNLOvbXf2TCgBA@mail.gmail.com). Switching to TLSv1.2 in order to more easily
see the handshake on the wire makes the problem go away, which
probably did not help my sense of growing insanity last July.

https://github.com/openssl/openssl/issues/6109

Matt Caswell appears to be convinced that SSL_set_SSL_CTX() is
fundamentally broken.

We briefly talked about this in Brussels, and I've been trying to find
proof. Attached are some (very rough) tests that might highlight an
issue.

Basically, the new tests set up three hosts in pg_hosts.conf: one with
no client CA, one with a valid client CA, and one with a
malfunctioning CA (root+server_ca, which can't verify our client
certs). Then it switches out the default CA underneath to make sure it
does not affect the visible behavior, since that CA should not
actually be used in the end.

Unfortunately, the failure modes change depending on the default CA.
If it's not a bug in my tests, I think this may be an indication that
SSL_set_SSL_CTX() isn't fully switching out the client verification
behavior? For example, if the default CA isn't set, the other hosts
don't appear to ask for a client certificate even if they need one.
And vice versa.

--

+           /*
+            * Set flag to remember whether CA store has been loaded into this
+            * SSL_context.
+            */
+           if (host->ssl_ca)

I think this should be `if (host->ssl_ca[0])` -- which, incidentally,
fixes one of the new failing tests on my machine.

int
be_tls_init(bool isServerStart)
+{
+   SSL_CTX    *ctx;
+   List       *sni_hosts = NIL;
+   HostsLine   line;

A pointer to `line` is passed down to ssl_init_context(), but it's
only been partially initialized on the stack. Can it be
zero-initialized here instead?

+       if (ssl_snimode == SSL_SNIMODE_STRICT)
+       {
+           ereport(COMMERROR,
+                   (errcode(ERRCODE_PROTOCOL_VIOLATION),
+                    errmsg("no hostname provided in callback")));
+           return SSL_TLSEXT_ERR_ALERT_FATAL;
+       }

At the moment we're sending an `unrecognized_name` alert in strict
mode if the client doesn't send SNI. RFC 8446 suggests
`missing_extension`:

Additionally, all implementations MUST support the use of the
"server_name" extension with applications capable of using it.
Servers MAY require clients to send a valid "server_name" extension.
Servers requiring this extension SHOULD respond to a ClientHello
lacking a "server_name" extension by terminating the connection with
a "missing_extension" alert.

Should we do that, or should we ignore the suggestion? The problem
with missing_extension, IMO, is that there's absolutely no indication
to the client as to which extension is missing. unrecognized_name is a
little confusing in this case (there was no name sent), but at least
the end user will be able to link that to an SNI problem via search
engine.

+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+                   # (change requires restart)

Nitpickiest nitpick: looks like the other lines use a tab instead of a
space between the setting and the trailing comment.

Thanks,
--Jacob

[1]: /messages/by-id/CAOYmi+nTwu7=aUGCkf6L-ULqS8itNP7uc9nUmNLOvbXf2TCgBA@mail.gmail.com

Attachments:

tests.patch.txttext/plain; charset=US-ASCII; name=tests.patch.txtDownload
commit a4d9cbf4d1228dcc17f2961b7811321a50e74617
Author: Jacob Champion <jacob.champion@enterprisedb.com>
Date:   Tue Mar 4 13:17:12 2025 -0800

    Tests

diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index f0ce048273a..72e64c6c00d 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -33,6 +33,11 @@ if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
 
 my $ssl_server = SSL::Server->new();
 
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+
 my $node = PostgreSQL::Test::Cluster->new('primary');
 $node->init;
 
@@ -161,4 +166,57 @@ $node->connect_fails(
 	"connect fails since the passphrase protected key cannot be reloaded",
 	expected_stderr => qr/tlsv1 unrecognized name/);
 
+# Test client CAs.
+
+$connstr =
+  "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+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;
+
+my @cases = ( "", "root+client_ca", "root+server_ca" );
+foreach my $default_ca (@cases)
+{
+	# The default CA should, ideally, not matter for the purposes of these
+	# tests, since we connect to the other hosts explicitly.
+	$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 " . sslkey('client.key'),
+		"example.org, $default_ca: connect with sslcert, no client CA configured",
+		expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
+
+	# example.com is configured and should require a valid client cert.
+	$node->connect_fails(
+		"$connstr host=example.com sslcertmode=disable",
+		"example.com, $default_ca: connect fails if no client certificate sent",
+		expected_stderr => qr/connection requires a valid client certificate/);
+
+	$node->connect_ok(
+		"$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+		"example.com, $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",
+		"example.net, $default_ca: connect fails if no client certificate sent",
+		expected_stderr => qr/connection requires a valid client certificate/);
+
+	$node->connect_fails(
+		"$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+		"example.net, $default_ca: connect with sslcert, client certificate sent",
+		expected_stderr => qr/unknown ca/);
+}
+
 done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index e044318531f..bdcce84003e 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -71,6 +71,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")
@@ -145,7 +146,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>
 
@@ -180,10 +182,11 @@ 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";
+	$sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"
+	  if $params->{cafile} ne "";
 	$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
 	  if defined $params->{crldir};
 
#14Andres Freund
andres@anarazel.de
In reply to: Daniel Gustafsson (#12)
Re: Serverside SNI support in libpq

Hi,

On 2025-02-27 14:38:24 +0100, Daniel Gustafsson wrote:

The attached v6 rebase contains this as well as your tests as well as
general cleanup and comment writing etc.

This is not passing CI on windows...
https://cirrus-ci.com/build/4765059278176256

Greetings,

Andres

#15Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#14)
1 attachment(s)
Re: Serverside SNI support in libpq

On 13 May 2025, at 15:46, Andres Freund <andres@anarazel.de> wrote:

This is not passing CI on windows...
https://cirrus-ci.com/build/4765059278176256

When looking into why the SNI tests failed on Windows I think I found a
pre-existing issue that we didn't have tests for, which my patch added tests
for and thus broke.

The test I added was to check restarting and reloading with ssl passphrase
commands (which we do have testcoverage for) with a subsequent connection test
to ensure it didn't just work to start the cluster.

When ssl_passphrase_command_supports_reload is set to 'off', the cluster should
allow connections until a reload has been issued. That works fine except on
Windows where our process-model is such that a new connection will re-run the
passphrase command, which inevitably fails as it's not configured for reload.
The test in my patch exposed this out of (happy) accident, but it can be
reproduced in HEAD as well. The attached version modifies the ssl tests to
cover this with a connection attempt. If I'm not mistaken though, there should
probably be a docs patch to make it clear how this works on Windows.

No codechanges on top of the test fix.

--
Daniel Gustafsson

Attachments:

v8-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v8-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From 42c1c44ebec355d48755d7821782d49a15568c77 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 2 Jun 2025 10:25:08 +0200
Subject: [PATCH v8] 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.

Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  66 ++++
 doc/src/sgml/runtime.sgml                     |  67 ++++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 203 +++++++++-
 src/backend/libpq/be-secure-openssl.c         | 356 ++++++++++++++++--
 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                  |  26 ++
 src/backend/utils/misc/guc_tables.c           |  31 ++
 src/backend/utils/misc/postgresql.conf.sample |   3 +
 src/bin/initdb/initdb.c                       |  16 +-
 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/001_ssltests.pl                |  29 +-
 src/test/ssl/t/004_sni.pl                     | 175 +++++++++
 src/test/ssl/t/SSL/Server.pm                  |   8 +
 src/tools/pgindent/typedefs.list              |   2 +
 23 files changed, 1007 insertions(+), 63 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 0a4b3e55ba5..17ba6538de6 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..fa6fe07adc0 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 defualt 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..67a50c7b24c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
 
 #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
@@ -37,19 +42,20 @@
  * 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 +181,196 @@ 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 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;
+	MemoryContext oldcxt;
+	MemoryContext hostcxt;
+
+	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);
+
+	hostcxt = AllocSetContextCreate(PostmasterContext,
+									"hosts file parser context",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostcxt);
+
+	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);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * 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("no SNI configuration added from configuration file  \"%s\"",
+					   HostsFileName));
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	if (!ok)
+	{
+		MemoryContextDelete(hostcxt);
+		return NIL;
+	}
+
+	return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index c8b63ef8249..14ff1d78c40 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
 #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;
+	bool		ssl_passphrase_support_reload;
+} 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 +82,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 +90,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);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -96,11 +111,160 @@ static const char *cert_errdetail;
 
 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.
+	 */
+	if (contexts != NIL)
+	{
+		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.
+	 */
+	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;
+
+		/*
+		 * Load pg_hosts.conf and parse each row, returning the set of hosts
+		 * as a list.
+		 */
+		sni_hosts = load_hosts();
+
+		/*
+		 * 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)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load %s", "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)
+			{
+				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.  The passphrase command fields are
+			 * however optional so we need to check whether those were set.
+			 */
+			host_context = palloc0(sizeof(HostContext));
+			host_context->hostname = pstrdup(host->hostname);
+			host_context->context = tmp_context;
+			host_context->default_host = false;
+			if (host->ssl_passphrase_cmd != NULL)
+				host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+			host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+			/*
+			 * Set flag to remember whether CA store has been loaded into this
+			 * SSL_context.
+			 */
+			if (host->ssl_ca)
+				host_context->ssl_loaded_verify_locations = true;
+
+			contexts = lappend(contexts, host_context);
+		}
+	}
+
+	/* 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
@@ -126,10 +290,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;
@@ -137,16 +308,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;
 
 	/*
@@ -155,19 +326,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;
 	}
 
@@ -319,17 +490,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;
 		}
 
@@ -401,38 +572,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
@@ -759,6 +921,9 @@ be_tls_close(Port *port)
 		pfree(port->peer_dn);
 		port->peer_dn = NULL;
 	}
+
+	Host_context = NULL;
+	SSL_context = NULL;
 }
 
 ssize_t
@@ -1132,7 +1297,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);
 }
 
 /*
@@ -1369,6 +1534,88 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+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.
+	 */
+	Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+	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)
+		{
+			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;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			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;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			return SSL_TLSEXT_ERR_OK;
+		}
+	}
+
+	/*
+	 * In ssl_snimode "strict" it's an error if there was no match for the
+	 * hostname in the TLS extension.  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;
+	SSL_set_SSL_CTX(ssl, SSL_context);
+	Assert(SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1578,6 +1825,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)
 {
@@ -1771,17 +2024,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
 
 			/*
@@ -1793,3 +2052,26 @@ 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));
+		if (host->ssl_passphrase)
+			pfree(unconstify(char *, host->ssl_passphrase));
+		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 46fdefebe35..ed670822ea3 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,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"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
 	}
 	SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
 
+	/*
+	 * 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_tables.c b/src/backend/utils/misc/guc_tables.c
index f137129209f..3339e829314 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
  */
@@ -555,6 +562,7 @@ char	   *cluster_name = "";
 char	   *ConfigFileName;
 char	   *HbaFileName;
 char	   *IdentFileName;
+char	   *HostsFileName;
 char	   *external_pid_file;
 
 char	   *application_name;
@@ -4787,6 +4795,17 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+			gettext_noop("Sets the server's \"hosts\" configuration file."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&HostsFileName,
+		NULL,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
 			gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5457,6 +5476,18 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the SNI mode to use."),
+			NULL,
+			GUC_SUPERUSER_ONLY,
+		},
+		&ssl_snimode,
+		SSL_SNIMODE_DEFAULT,
+		ssl_snimode_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
 			gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a9d8293474a..57b1be3c38f 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..087cea4fffc 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,13 @@ 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\n"
+				"PG_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 +2830,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 3657f182db3..3d8e33533b8 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 aeb66ca40cf..5feed0eb0a4 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;
@@ -134,12 +135,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 f619100467d..025e7e95e90 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,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/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b2eb18d3e81..0f0f64b6c7c 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -51,8 +51,15 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_sslcertmode_require =
   check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
 
-# Allocation of base connection string shared among multiple tests.
-my $common_connstr;
+# Set of default settings for SSL parameters in connection string.  This
+# makes the tests protected against any defaults the environment may have
+# in ~/.postgresql/.
+my $default_ssl_connstr =
+  "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
+
+# Base connection string shared among multiple tests.
+my $common_connstr =
+  "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
 
 #### Set up the server.
 
@@ -85,7 +92,7 @@ switch_server_cert(
 	passphrase_cmd => 'echo wrongpassword',
 	restart => 'no');
 
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(fail_ok => 1, log_like => qr/could not load private key file/);
 is($result, 0,
 	'restart fails with password-protected key file with wrong password');
 
@@ -95,11 +102,16 @@ switch_server_cert(
 	cafile => 'root+client_ca',
 	keyfile => 'server-password',
 	passphrase_cmd => 'echo secret1',
+	passphrase_cmd_reload => 'yes',
 	restart => 'no');
 
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(fail_ok => 1, log_unlike => qr/could not load private key file/);
 is($result, 1, 'restart succeeds with password-protected key file');
 
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
 # Test compatibility of SSL protocols.
 # TLSv1.1 is lower than TLSv1.2, so it won't work.
 $node->append_conf(
@@ -139,15 +151,6 @@ note "running client tests";
 
 switch_server_cert($node, certfile => 'server-cn-only');
 
-# Set of default settings for SSL parameters in connection string.  This
-# makes the tests protected against any defaults the environment may have
-# in ~/.postgresql/.
-my $default_ssl_connstr =
-  "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
-
-$common_connstr =
-  "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
-
 SKIP:
 {
 	skip "Keylogging is not supported with LibreSSL", 5 if $libressl;
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..b3b2821a2bd
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,175 @@
+
+# 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/tlsv1 unrecognized name/);
+
+# 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",
+	"1 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",
+	"1 connect with correct server CA cert file sslmode=require");
+
+# 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");
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index efbd0dafaf6..31e50c8722f 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -296,6 +296,11 @@ The CRL directory to use. Implementation is SSL backend specific.
 The passphrase command to use. If not set, an empty passphrase command will
 be set.
 
+=item passphrase_cmd_reload => B<value>
+
+Whether or not to allow passphrase command reloading. If set the passphrase
+command will set to allow reloading.
+
 =item restart => B<value>
 
 If set to 'no', the server won't be restarted after updating the settings.
@@ -327,6 +332,9 @@ sub switch_server_cert
 		"ssl_passphrase_command='" . $params{passphrase_cmd} . "'")
 	  if defined $params{passphrase_cmd};
 
+	$node->append_conf('sslconfig.conf', 'ssl_passphrase_command_supports_reload=on')
+	  if defined $params{passphrase_cmd_reload};
+
 	return if (defined($params{restart}) && $params{restart} eq 'no');
 
 	$node->restart;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..5b61b9a1d4d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1202,6 +1202,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#16Michael Paquier
michael@paquier.xyz
In reply to: Daniel Gustafsson (#15)
Re: Serverside SNI support in libpq

On Wed, Aug 27, 2025 at 09:49:34PM +0200, Daniel Gustafsson wrote:

When looking into why the SNI tests failed on Windows I think I found a
pre-existing issue that we didn't have tests for, which my patch added tests
for and thus broke.

The test I added was to check restarting and reloading with ssl passphrase
commands (which we do have testcoverage for) with a subsequent connection test
to ensure it didn't just work to start the cluster.

Would this part be better if extracted from the main patch and then
backpatched? Even if not backpatched, a split would be cleaner on
HEAD, I assume, leading to less fuzz with the main patch.
--
Michael

#17Daniel Gustafsson
daniel@yesql.se
In reply to: Michael Paquier (#16)
Re: Serverside SNI support in libpq

On 1 Sep 2025, at 03:58, Michael Paquier <michael@paquier.xyz> wrote:

On Wed, Aug 27, 2025 at 09:49:34PM +0200, Daniel Gustafsson wrote:

When looking into why the SNI tests failed on Windows I think I found a
pre-existing issue that we didn't have tests for, which my patch added tests
for and thus broke.

The test I added was to check restarting and reloading with ssl passphrase
commands (which we do have testcoverage for) with a subsequent connection test
to ensure it didn't just work to start the cluster.

Would this part be better if extracted from the main patch and then
backpatched? Even if not backpatched, a split would be cleaner on
HEAD, I assume, leading to less fuzz with the main patch.

Yes, that's my plan, just wanted to float it here first to see if I was
thinking about it all wrong. I will raise it on its own thread on -hackers.
The backpatchable portion is probably limited to a docs entry clarifying the
behaviour on Windows.

--
Daniel Gustafsson

#18Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#17)
1 attachment(s)
Re: Serverside SNI support in libpq

Attached is a cleaned up rebase with improved memory handling, additional code
documentation, removed passphrase test (sent as a separate thread), and some
general cleanup and additional testing.

--
Daniel Gustafsson

Attachments:

v9-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v9-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From 542ffd45c914597821f258e4b838371e03abc32e Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 2 Jun 2025 10:25:08 +0200
Subject: [PATCH v9] 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 <daniel@yesql.se>
Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 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         | 382 ++++++++++++++++--
 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                     | 175 ++++++++
 src/tools/pgindent/typedefs.list              |   2 +
 22 files changed, 1005 insertions(+), 51 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 683f7c36f46..ed6d42a1561 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 c8b63ef8249..ea3ba012b86 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);
 
 /* for passing data back from verify_cb() */
 static const char *cert_errdetail;
@@ -96,11 +110,173 @@ static const char *cert_errdetail;
 
 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.
+	 */
+	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"));
+			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.  The passphrase command fields are
+			 * however optional so we need to check whether those were set.
+			 */
+			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_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
@@ -126,10 +302,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;
@@ -137,16 +320,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;
 
 	/*
@@ -155,19 +338,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;
 	}
 
@@ -319,17 +502,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;
 		}
 
@@ -401,38 +584,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
@@ -759,6 +933,9 @@ be_tls_close(Port *port)
 		pfree(port->peer_dn);
 		port->peer_dn = NULL;
 	}
+
+	Host_context = NULL;
+	SSL_context = NULL;
 }
 
 ssize_t
@@ -1132,7 +1309,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);
 }
 
 /*
@@ -1369,6 +1546,104 @@ 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)
+		{
+			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;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			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;
+			SSL_set_SSL_CTX(ssl, SSL_context);
+			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;
+	SSL_set_SSL_CTX(ssl, SSL_context);
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1578,6 +1853,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)
 {
@@ -1771,17 +2052,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
 
 			/*
@@ -1793,3 +2080,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 679846da42c..ec92c0739b3 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 f62b61967ef..5828516c4e3 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..b3b2821a2bd
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,175 @@
+
+# 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/tlsv1 unrecognized name/);
+
+# 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",
+	"1 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",
+	"1 connect with correct server CA cert file sslmode=require");
+
+# 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");
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 432509277c9..fc66acb2f12 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)

#19Chao Li
li.evan.chao@gmail.com
In reply to: Daniel Gustafsson (#18)
Re: Serverside SNI support in libpq

Hi Daniel,

I just reviewed the patch and got a few comments:

On Nov 11, 2025, at 06:32, Daniel Gustafsson <daniel@yesql.se> wrote:

Attached is a cleaned up rebase with improved memory handling, additional code
documentation, removed passphrase test (sent as a separate thread), and some
general cleanup and additional testing.

--
Daniel Gustafsson

<v9-0001-Serverside-SNI-support-for-libpq.patch>

1 - commit message
```
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
```

Typo: certicate -> certificate

2 - be-secure-common.c
```
+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;
```

Cmd is only passed to replace_percent_placeholders(), and the function take a "const char *” argument, so we can define cmd as “const char *”.

2 - be-secure-common.c
```
+	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);
```

When I read this function, I thought to raise a comment for freeing hosts_lines. However, then I read be-secure-openssl.c, I saw the load_hosts() is called within a transient hostctx, so it doesn’t have to free memory pieces. Can we explain that in the function comment? Otherwise other reviewers and future code readers may have the same confusion.

3 - be-secure-openssl.c
```
int
@@ -759,6 +933,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
```

Do we need to free_contexts() here? When be_tls_init() is called again, contexts will anyway be freed, so I guess earlier free might be better?

4 - guc_parameters.dat
```
+{ 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 => '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',
+},
```

If ssl_snimode is PGC_SIGHUP that allows to reload without a server reset, why hosts_file cannot?

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

#20Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#18)
Re: Serverside SNI support in libpq

On Mon, Nov 10, 2025 at 2:33 PM Daniel Gustafsson <daniel@yesql.se> wrote:

Attached is a cleaned up rebase with improved memory handling, additional code
documentation, removed passphrase test (sent as a separate thread), and some
general cleanup and additional testing.

Thanks! Builds and passes back to OpenSSL 1.1.1 and LibreSSL 3.4
(except for the unrelated known issue with "depth 0"/"depth 1", which
this patch did not introduce [1]/messages/by-id/CAOYmi+k=VF-2BCqfR49A92tx=_QNuL=3iT3w6FysOffKw9cxDQ@mail.gmail.com).

Did you have any thoughts on my earlier review [2]? The test patch
attached there still fails on my machine with v9.

Thanks,
--Jacob

[1]: /messages/by-id/CAOYmi+k=VF-2BCqfR49A92tx=_QNuL=3iT3w6FysOffKw9cxDQ@mail.gmail.com
[1]: /messages/by-id/CAOYmi+k=VF-2BCqfR49A92tx=_QNuL=3iT3w6FysOffKw9cxDQ@mail.gmail.com

#21Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Chao Li (#19)
Re: Serverside SNI support in libpq

On Tue, Nov 11, 2025 at 1:07 AM Chao Li <li.evan.chao@gmail.com> wrote:

If ssl_snimode is PGC_SIGHUP that allows to reload without a server reset, why hosts_file cannot?

I think all of our FILE_LOCATIONS GUCs are handled similarly.

--Jacob

#22Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#20)
Re: Serverside SNI support in libpq

On 12 Nov 2025, at 23:44, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

Did you have any thoughts on my earlier review [2]? The test patch
attached there still fails on my machine with v9.

Oh shoot, I missed that when going back over the thread. Will have a look.

--
Daniel Gustafsson

#23Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#20)
1 attachment(s)
Re: Serverside SNI support in libpq

On 12 Nov 2025, at 23:44, Jacob Champion <jacob.champion@enterprisedb.com> 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:

v10-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v10-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From 11984e057105c89d7f02f8f0be2a736ababee1f1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
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 <daniel@yesql.se>
Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 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)

#24Chao Li
li.evan.chao@gmail.com
In reply to: Daniel Gustafsson (#23)
Re: Serverside SNI support in libpq

Hi Daniel,

None of my comment on v9 are addressed in v10:

1 - commit message
```
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
```

Typo: certicate -> certificate

2 - be-secure-common.c
```
+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;
```

Cmd is only passed to replace_percent_placeholders(), and the function take a "const char *” argument, so we can define cmd as “const char *”.

2 - be-secure-common.c
```
+ 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);
```

When I read this function, I thought to raise a comment for freeing hosts_lines. However, then I read be-secure-openssl.c, I saw the load_hosts() is called within a transient hostctx, so it doesn’t have to free memory pieces. Can we explain that in the function comment? Otherwise other reviewers and future code readers may have the same confusion.

3 - be-secure-openssl.c
```
int
@@ -759,6 +933,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
```

Do we need to free_contexts() here? When be_tls_init() is called again, contexts will anyway be freed, so I guess earlier free might be better?

4 - guc_parameters.dat
```
+{ 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 => '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',
+},
```

If ssl_snimode is PGC_SIGHUP that allows to reload without a server reset, why hosts_file cannot?

Comment 4 can be ignored as Jacob has answered.

On Nov 24, 2025, at 22:53, Daniel Gustafsson <daniel@yesql.se> wrote:

On 12 Nov 2025, at 23:44, Jacob Champion <jacob.champion@enterprisedb.com> 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

<v10-0001-Serverside-SNI-support-for-libpq.patch>

I reviewed v10 again, and got some a few more comments:

5 - runtime.sgml
```
+    in <filename>pg_hba.conf</filename>.  <replaceable>hostname</replaceable>
+    is matched against the hostname TLS extension in the SSL handshake.
```

In the patch code, default context uses hostname “*”, should we explain “*” here in the doc?

6 - runtime.sgml
```
+    <filename>pg_hosts.conf</filename>, which is stored in the clusters
+    data directory.  The hosts configuration file contains lines of the general
```

Typo: clusters => cluster’s

7 - runtime.sgml
```
+ will only be used to for the handshake until the hostname is inspected, it
```

“Used to for” => “used for"

8 - Cluster.pm
```
+matching the specified pattern. If the pattern matches agsinst the logfile a
```

Typo: agsinst => against

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

#25Daniel Gustafsson
daniel@yesql.se
In reply to: Chao Li (#24)
1 attachment(s)
Re: Serverside SNI support in libpq

On 25 Nov 2025, at 00:28, Chao Li <li.evan.chao@gmail.com> wrote:

Hi Daniel,

None of my comment on v9 are addressed in v10:

I do apologise, I was so focused on fixing Jacob's tests that I forgot about
addressing these. Please find the attached v11 with your comments addressed.
Thank you for all your review, much appreciated!

1 - commit message
```
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
```

Typo: certicate -> certificate

Fixed. I also reworded the commit message from saying experimental since we
don't have a concept of experimental features really.

2 - be-secure-common.c
```
+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;
```

Cmd is only passed to replace_percent_placeholders(), and the function take a "const char *” argument, so we can define cmd as “const char *”.

Fixed.

2 - be-secure-common.c
```
+ 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);
```

When I read this function, I thought to raise a comment for freeing hosts_lines. However, then I read be-secure-openssl.c, I saw the load_hosts() is called within a transient hostctx, so it doesn’t have to free memory pieces. Can we explain that in the function comment? Otherwise other reviewers and future code readers may have the same confusion.

I expanded the comment, and while there also improved the error reporting from
the function by returning a bool indicating status as well as the list (since
NIL was both empty-file and error).

3 - be-secure-openssl.c
```
int
@@ -759,6 +933,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
```

Do we need to free_contexts() here? When be_tls_init() is called again, contexts will anyway be freed, so I guess earlier free might be better?

I don't think so, be_tls_close is only for closing the session.

I reviewed v10 again, and got some a few more comments:

5 - runtime.sgml
```
+    in <filename>pg_hba.conf</filename>.  <replaceable>hostname</replaceable>
+    is matched against the hostname TLS extension in the SSL handshake.
```

In the patch code, default context uses hostname “*”, should we explain “*” here in the doc?

I don't think we should since we don't want anyone to configure a host with
'*'. That does bring up a good point though, and I added a check in the
parsing to ensure that such wildcard hostnames cause failures in parsing if
found in pg_hosts.

6 - runtime.sgml
```
+    <filename>pg_hosts.conf</filename>, which is stored in the clusters
+    data directory.  The hosts configuration file contains lines of the general
```

Typo: clusters => cluster’s

Fixed.

7 - runtime.sgml
```
+ will only be used to for the handshake until the hostname is inspected, it
```

“Used to for” => “used for"

Fixed.

8 - Cluster.pm
```
+matching the specified pattern. If the pattern matches agsinst the logfile a
```

Typo: agsinst => against

Fixed.

--
Daniel Gustafsson

Attachments:

v11-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v11-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From dee74be3d7d00363a444e6c56d60d3daa5abdda8 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Tue, 25 Nov 2025 15:33:18 +0100
Subject: [PATCH v11] Serverside SNI support for libpq

Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certficate/key handling
is available per host.  A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname.  A new GUC, ssl_snimode, is used to control how the hostname
TLS extension is handled.  The possible values are off (which is used
as the new backwards compatible default), default and strict:

  - off: pg_hosts.conf is not parsed and the hostname TLS extension is
    not inspected at all.  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 <daniel@yesql.se>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  66 +++
 doc/src/sgml/runtime.sgml                     |  67 +++
 src/backend/Makefile                          |   1 +
 src/backend/libpq/be-secure-common.c          | 223 ++++++++-
 src/backend/libpq/be-secure-openssl.c         | 451 ++++++++++++++++--
 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, 1160 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..687aa86f68a 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 cluster's
+    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 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..8d5a0c1ba2c 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;
+	const char *cmd = (const 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,212 @@ 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);
+	if (strcmp(parsedline->hostname, "*") == 0)
+	{
+		ereport(elevel,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("wildcard hostname not allowed in hosts configuration"),
+				errcontext("line %d of configuration file \"%s\"",
+						   tok_line->line_num, tok_line->file_name));
+		return NULL;
+	}
+
+	/* 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 and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file.  The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false.  or an empty file.  This
+ * function is intended to be executed within a temporary memory context which
+ * can be discarded to free memory allocated during the processing of the file.
+ */
+bool
+load_hosts(List **hosts)
+{
+	FILE	   *file;
+	ListCell   *line;
+	List	   *hosts_lines = NIL;
+	List	   *parsed_lines = NIL;
+	HostsLine  *newline;
+	bool		ok = true;
+
+	if (hosts)
+		*hosts = NIL;
+
+	/*
+	 * 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 false;
+	}
+
+	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 might not be an error
+	 * since the pg_hosts file is additive to the default SSL configuration in
+	 * some ssl_sni settings.
+	 */
+	if (ok && parsed_lines == NIL)
+	{
+		ereport(DEBUG1,
+				errmsg("SNI configuration not found in configuration file  \"%s\"",
+					   HostsFileName));
+	}
+
+	if (!ok)
+		return false;
+
+	if (hosts)
+		*hosts = parsed_lines;
+
+	return true;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..ac3984bc275 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,195 @@ struct CallbackErr
 
 int
 be_tls_init(bool isServerStart)
+{
+	SSL_CTX    *ctx;
+	List	   *sni_hosts = NIL;
+	HostsLine	line;
+	bool		res;
+
+	/*
+	 * 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);
+		res = load_hosts(&sni_hosts);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * If loading failed, then make sure to error out regardless. It's not
+		 * really an error to not have a hosts file in non-strict modes but if
+		 * there is one and it fails to load properly, then silently pressing
+		 * on seems worse than raising an error.
+		 */
+		if (!res)
+		{
+			MemoryContextDelete(hostcxt);
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load %s", "pg_hosts.conf"));
+			return -1;
+		}
+
+		/*
+		 * 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;
+		}
+
+		/*
+		 * Loading and parsing the hosts file was successful, create contexts
+		 * for each host entry and add to the the list of host to be checked
+		 * during login.
+		 */
+		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 +330,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 +348,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 +366,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 +530,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 +553,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 +616,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 +971,9 @@ be_tls_close(Port *port)
 		pfree(port->peer_dn);
 		port->peer_dn = NULL;
 	}
+
+	Host_context = NULL;
+	SSL_context = NULL;
 }
 
 ssize_t
@@ -1144,7 +1347,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 +1593,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 +1927,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 +2126,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 +2154,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..f6c1422b555 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_OFF;
+
 /* ------------------------------------------------------------ */
 /*			 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..a895067f7ad 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_OFF',
+  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..c5ff8302201 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 = off
 
 
 #------------------------------------------------------------------------------
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..54d8ee8a2aa 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 bool load_hosts(List **hosts);
 
 #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..55e0f04d4f5 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 against 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..bbd3bed6c86 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)

#26Dewei Dai
daidewei1970@163.com
In reply to: Daniel Gustafsson (#5)
Re: Re: Serverside SNI support in libpq

Hi Daniel,
I just reviewed the v11 patch and got a few comments:

1 - commit message
```This adds support for serverside SNI such that certficate/key handling
```
Typo: certficate -> certificate

2 -be-secure-openssl.c
```* host/snimode match, but we need something to drive the hand- shake till
```
Typo: hand- shake ->handshake

3 - be-secure-openssl.c
```
errhint("In strict ssl_snimode there need to be at least one entry in pg_hosts.conf."));
there needs to be
```
Typo: There need to be -> there needs to be

4 - src/backend/makefile
It is recommended to delete pg_hosts.conf.sample during the `make uninstall` command

5  - be-secure-openssl.c
```
be_tls_destroy(void)
 {
+ ListCell   *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
`````
In the `be_tls_destroy` function, the context is released, but it is not set to null. 
         This is similar to the `free_context` function, and it seems that it can be called directly.

Best regards

daidewei1970@163.com

#27Daniel Gustafsson
daniel@yesql.se
In reply to: Dewei Dai (#26)
1 attachment(s)
Re: Serverside SNI support in libpq

On 26 Nov 2025, at 10:14, Dewei Dai <daidewei1970@163.com> wrote:

Hi Daniel,
I just reviewed the v11 patch and got a few comments:

Thanks!

Typo: certficate -> certificate

Fixed.

Typo: hand- shake ->handshake

Fixed.

Typo: There need to be -> there needs to be

AFAIK "need to be" is the correct spelling for referring to a singular thing,
and "needs to be" is correct for plural. I've been thinking about this in a
singular context but maybe "needs to be" is the right wording since the hint is
"at least one". Changed to "needs to be" just in case.

It is recommended to delete pg_hosts.conf.sample during the `make uninstall` command

Nice catch, fixed.

In the `be_tls_destroy` function, the context is released, but it is not set to null.
This is similar to the `free_context` function, and it seems that it can be called directly.

That's a good point, be_tls_destroy can just call free_contexts directly and
save some code while making sure it's consistent. Fixed.

--
Daniel Gustafsson

Attachments:

v12-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v12-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From d3eecf4ec13de48e8729c92670560ce79a15b6d1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 26 Nov 2025 15:30:56 +0100
Subject: [PATCH v12] Serverside SNI support for libpq

Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certificate/key handling
is available per host.  A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname.  A new GUC, ssl_snimode, is used to control how the hostname
TLS extension is handled.  The possible values are off (which is used
as the new backwards compatible default), default and strict:

  - off: pg_hosts.conf is not parsed and the hostname TLS extension is
    not inspected at all.  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 <daniel@yesql.se>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Dewei Dai <daidewei1970@163.com>
Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/config.sgml                      |  66 +++
 doc/src/sgml/runtime.sgml                     |  67 +++
 src/backend/Makefile                          |   2 +
 src/backend/libpq/be-secure-common.c          | 223 ++++++++-
 src/backend/libpq/be-secure-openssl.c         | 443 ++++++++++++++++--
 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, 1152 insertions(+), 63 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 737b90736bf..acae1601d39 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1700,6 +1700,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..687aa86f68a 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 cluster's
+    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 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..529126eebeb 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)
@@ -246,6 +247,7 @@ endif
 	$(MAKE) -C utils uninstall-data
 	rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \
 	      '$(DESTDIR)$(datadir)/pg_ident.conf.sample' \
+	      '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \
 	      '$(DESTDIR)$(datadir)/postgresql.conf.sample'
 ifeq ($(with_llvm), yes)
 	$(call uninstall_llvm_module,postgres)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..8d5a0c1ba2c 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;
+	const char *cmd = (const 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,212 @@ 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);
+	if (strcmp(parsedline->hostname, "*") == 0)
+	{
+		ereport(elevel,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("wildcard hostname not allowed in hosts configuration"),
+				errcontext("line %d of configuration file \"%s\"",
+						   tok_line->line_num, tok_line->file_name));
+		return NULL;
+	}
+
+	/* 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 and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file.  The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false.  or an empty file.  This
+ * function is intended to be executed within a temporary memory context which
+ * can be discarded to free memory allocated during the processing of the file.
+ */
+bool
+load_hosts(List **hosts)
+{
+	FILE	   *file;
+	ListCell   *line;
+	List	   *hosts_lines = NIL;
+	List	   *parsed_lines = NIL;
+	HostsLine  *newline;
+	bool		ok = true;
+
+	if (hosts)
+		*hosts = NIL;
+
+	/*
+	 * 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 false;
+	}
+
+	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 might not be an error
+	 * since the pg_hosts file is additive to the default SSL configuration in
+	 * some ssl_sni settings.
+	 */
+	if (ok && parsed_lines == NIL)
+	{
+		ereport(DEBUG1,
+				errmsg("SNI configuration not found in configuration file  \"%s\"",
+					   HostsFileName));
+	}
+
+	if (!ok)
+		return false;
+
+	if (hosts)
+		*hosts = parsed_lines;
+
+	return true;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..678d1587e44 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,195 @@ struct CallbackErr
 
 int
 be_tls_init(bool isServerStart)
+{
+	SSL_CTX    *ctx;
+	List	   *sni_hosts = NIL;
+	HostsLine	line;
+	bool		res;
+
+	/*
+	 * 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 handshake 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);
+		res = load_hosts(&sni_hosts);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * If loading failed, then make sure to error out regardless. It's not
+		 * really an error to not have a hosts file in non-strict modes but if
+		 * there is one and it fails to load properly, then silently pressing
+		 * on seems worse than raising an error.
+		 */
+		if (!res)
+		{
+			MemoryContextDelete(hostcxt);
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load %s", "pg_hosts.conf"));
+			return -1;
+		}
+
+		/*
+		 * 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 needs to be at least one entry in pg_hosts.conf."));
+			return -1;
+		}
+
+		/*
+		 * Loading and parsing the hosts file was successful, create contexts
+		 * for each host entry and add to the the list of host to be checked
+		 * during login.
+		 */
+		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 +330,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 +348,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 +366,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 +530,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 +553,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 +616,19 @@ 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);
-	SSL_context = NULL;
-	ssl_loaded_verify_locations = false;
+	free_contexts();
 }
 
 int
@@ -771,6 +961,9 @@ be_tls_close(Port *port)
 		pfree(port->peer_dn);
 		port->peer_dn = NULL;
 	}
+
+	Host_context = NULL;
+	SSL_context = NULL;
 }
 
 ssize_t
@@ -1144,7 +1337,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 +1583,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 +1917,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 +2116,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 +2144,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..f6c1422b555 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_OFF;
+
 /* ------------------------------------------------------------ */
 /*			 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 3b9d8349078..30cde4ee800 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1167,6 +1167,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',
@@ -2742,6 +2749,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_OFF',
+  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 f87b558c2c6..cd2074d54e1 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..c5ff8302201 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 = off
 
 
 #------------------------------------------------------------------------------
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..54d8ee8a2aa 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 bool load_hosts(List **hosts);
 
 #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..55e0f04d4f5 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 against 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..bbd3bed6c86 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 dfcd619bfee..9c580db6b7d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1208,6 +1208,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#28Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Daniel Gustafsson (#27)
Re: Serverside SNI support in libpq

Sorry for jumping in so late.

On Fri, May 10, 2024 at 7:23 AM Daniel Gustafsson
<daniel(at)yesql(dot)se> wrote:

The attached patch adds serverside SNI support to libpq, it is still a bit
rough around the edges but I'm sharing it early to make sure I'm not designing
it in a direction that the community doesn't like. A new config file
$datadir/pg_hosts.conf is used for configuring which certicate and key should
be used for which hostname. The file is parsed in the same way as pg_ident
et.al so it allows for the usual include type statements we support. 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.

As of now the patch use default as the initial value for the GUC

Do we need the GUC? It feels a little confusing that a GUC affects how
the settings in the pg_hosts.conf are interepreted. It'd be nice if you
could open pg_hosts.conf in an editor, and see at one glance everything
that affects this.

I propose that there is no GUC. In 'pg_hosts.conf', you can specify a
wildcard '*' host that matches anything. You can also specify a "no sni"
line which matches connections with no SNI specified. (Or something
along those lines, I didn't think too hard about all the interactions).

Should we support wildcards like "*.example.com* too?

For backwards-compatibility, if you specify a certificate and key in
postgresql.conf, they are treated the same as if you had a "*" line in
pg_hosts.conf.

- Heikki

#29Daniel Gustafsson
daniel@yesql.se
In reply to: Heikki Linnakangas (#28)
Re: Serverside SNI support in libpq

On 3 Dec 2025, at 10:57, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

Sorry for jumping in so late.

Not at all, thanks for looking!

Do we need the GUC? It feels a little confusing that a GUC affects how the settings in the pg_hosts.conf are interepreted. It'd be nice if you could open pg_hosts.conf in an editor, and see at one glance everything that affects this.

I added the GUC for two reasons; as a way to opt-out of this feature if it's
something that the admin doesn't want; and as a way to set the SNI mode. There
are currently the two modes of STRICT and DEFAULT which affects how incoming
connections are handled. The first motivation might be unfounded, and the
second one could be encoded in a pg_hosts configuration though implicitly
rather than explicitly.

Having all the details in pg_hosts.conf is appealing, no disagreement there,
but it does pose some challenges in the interaction with the postgresql.conf
GUCS (more later).

I propose that there is no GUC. In 'pg_hosts.conf', you can specify a wildcard '*' host that matches anything. You can also specify a "no sni" line which matches connections with no SNI specified. (Or something along those lines, I didn't think too hard about all the interactions).

So basically reserving a hostname,"no_sni" or something, which indicates that
it's for non sslsni connections? That should work, with the parsing rule that
there can only be one in the file.

Should we support wildcards like "*.example.com* too?

I have that on my if-it-gets-committed TODO but I kept it out of the initial
proposal to keep complexity down and goalposts in sight.

For backwards-compatibility, if you specify a certificate and key in postgresql.conf, they are treated the same as if you had a "*" line in pg_hosts.conf.

That's a bit trickier though, since the cert/key have a default boot_val so
they will always be set to something unless the user enables ssl=on and at the
same time uncomments ssl_cert_file/ssl_key_file and set them to '' before
proceeding to add configuration in pg_hosts.conf. This is pretty unintuitive I
think. unintuitive. This backwards comatibility is one of the reasons I kept
the postgresl.conf values for the default context config.

I really want to make it possible for anyone who don't want SNI to keep using
postgresql.conf and get the exact behavior they've always had. Do you agree
with that design goal?

--
Daniel Gustafsson

#30Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Daniel Gustafsson (#29)
Re: Serverside SNI support in libpq

On 03/12/2025 18:52, Daniel Gustafsson wrote:

On 3 Dec 2025, at 10:57, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
Do we need the GUC? It feels a little confusing that a GUC affects how the settings in the pg_hosts.conf are interepreted. It'd be nice if you could open pg_hosts.conf in an editor, and see at one glance everything that affects this.

I added the GUC for two reasons; as a way to opt-out of this feature if it's
something that the admin doesn't want; and as a way to set the SNI mode. There
are currently the two modes of STRICT and DEFAULT which affects how incoming
connections are handled. The first motivation might be unfounded, and the
second one could be encoded in a pg_hosts configuration though implicitly
rather than explicitly.

Having all the details in pg_hosts.conf is appealing, no disagreement there,
but it does pose some challenges in the interaction with the postgresql.conf
GUCS (more later).

I propose that there is no GUC. In 'pg_hosts.conf', you can specify a wildcard '*' host that matches anything. You can also specify a "no sni" line which matches connections with no SNI specified. (Or something along those lines, I didn't think too hard about all the interactions).

So basically reserving a hostname,"no_sni" or something, which indicates that
it's for non sslsni connections? That should work, with the parsing rule that
there can only be one in the file.

Yeah, something like that. And to implement the "strict" mode, you could
have a "no_sni" line with no cert/key specified.

For backwards-compatibility, if you specify a certificate and key in postgresql.conf, they are treated the same as if you had a "*" line in pg_hosts.conf.

That's a bit trickier though, since the cert/key have a default boot_val so
they will always be set to something unless the user enables ssl=on and at the
same time uncomments ssl_cert_file/ssl_key_file and set them to '' before
proceeding to add configuration in pg_hosts.conf. This is pretty unintuitive I
think. unintuitive. This backwards comatibility is one of the reasons I kept
the postgresl.conf values for the default context config.

I really want to make it possible for anyone who don't want SNI to keep using
postgresql.conf and get the exact behavior they've always had. Do you agree
with that design goal?

Yeah, that's fair.

- Heikki

#31Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Heikki Linnakangas (#30)
Re: Serverside SNI support in libpq

On Wed, 3 Dec 2025 at 17:57, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I really want to make it possible for anyone who don't want SNI to keep using
postgresql.conf and get the exact behavior they've always had. Do you agree
with that design goal?

Yeah, that's fair.

What if we make it so that if a pg_hosts.conf file exists, then the
ssl_cert_file/ssl_key_file configs are ignored? And by default initdb
would not create a file (or it would, but with the same default
settings that we have now). Then we don't need the new GUC. Basically
it would be:
1. If the file does not exist, use the "off" behaviour
2. If the file exists, use the "strict" behaviour

#32Daniel Gustafsson
daniel@yesql.se
In reply to: Jelte Fennema-Nio (#31)
Re: Serverside SNI support in libpq

On 3 Dec 2025, at 22:27, Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

On Wed, 3 Dec 2025 at 17:57, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I really want to make it possible for anyone who don't want SNI to keep using
postgresql.conf and get the exact behavior they've always had. Do you agree
with that design goal?

Yeah, that's fair.

What if we make it so that if a pg_hosts.conf file exists, then the
ssl_cert_file/ssl_key_file configs are ignored? And by default initdb
would not create a file (or it would, but with the same default
settings that we have now).

Maybe. I'm not a big fan of magic-file-exist configurations but.. I'm trying
out a few different options to see which seems the most reasonable, and this is
for one of them.

Basically it would be:
1. If the file does not exist, use the "off" behaviour
2. If the file exists, use the "strict" behaviour

It will really be "strict" *or* "default" based on whether or not '*' is set as
a wildcard hostname (which can be argued is just a version of strict).

--
Daniel Gustafsson

#33Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#32)
Re: Serverside SNI support in libpq

Hi!

On Mon, Nov 24, 2025 at 6:53 AM Daniel Gustafsson <daniel@yesql.se> wrote:

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 new v12 tests still don't pass for me (they all use "certificate
verify failed", but the failure modes should be different).

+ if (host->ssl_ca && host->ssl_ca[0] != '\0')

The comment for HostsLine.ssl_ca, and the code that assigns it,
implies to me that host->ssl_ca should never be NULL. Am I missing a
case where it could be?

On Wed, Dec 3, 2025 at 1:57 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I propose that there is no GUC. In 'pg_hosts.conf', you can specify a
wildcard '*' host that matches anything. You can also specify a "no sni"
line which matches connections with no SNI specified. (Or something
along those lines, I didn't think too hard about all the interactions).

That seems to position SNI as a feature that every DBA should have to
think about by default. ("learn this file. you can't turn it off.") Is
it, yet?

Web servers enable SNI implicitly because name-based hosting is a
top-level concept for users over there (hostnames are baked into the
application layer). I would argue that we don't have that here. Maybe
in the future someone will ask for that, but at that point don't you
want a very different, name-based, config system?

On Wed, Dec 3, 2025 at 3:28 PM Daniel Gustafsson <daniel@yesql.se> wrote:

On 3 Dec 2025, at 22:27, Jelte Fennema-Nio <postgres@jeltef.nl> wrote:
What if we make it so that if a pg_hosts.conf file exists, then the
ssl_cert_file/ssl_key_file configs are ignored? And by default initdb
would not create a file (or it would, but with the same default
settings that we have now).

Maybe. I'm not a big fan of magic-file-exist configurations

Me neither. (I especially don't like the idea of ignoring a
certificate+key setting that a user has taken the time to put into a
config.)

Thanks,
--Jacob

#34Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#33)
Re: Serverside SNI support in libpq

On 11 Dec 2025, at 18:47, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

The new v12 tests still don't pass for me (they all use "certificate
verify failed", but the failure modes should be different).

In which version of OpenSSL (or LibreSSL)?

--
Daniel Gustafsson

#35Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#34)
Re: Serverside SNI support in libpq

On Thu, Dec 11, 2025 at 9:52 AM Daniel Gustafsson <daniel@yesql.se> wrote:

The new v12 tests still don't pass for me (they all use "certificate
verify failed", but the failure modes should be different).

In which version of OpenSSL (or LibreSSL)?

1.1.1 through 3.6. The CI for this commitfest entry shows it too:

https://cirrus-ci.com/task/5648027525840896

Local diff that missed `git add`, maybe?

--Jacob

#36Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#33)
1 attachment(s)
Re: Serverside SNI support in libpq

On 11 Dec 2025, at 18:47, Jacob Champion <jacob.champion@enterprisedb.com> wrote:
On Mon, Nov 24, 2025 at 6:53 AM Daniel Gustafsson <daniel@yesql.se> wrote:

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 new v12 tests still don't pass for me (they all use "certificate
verify failed", but the failure modes should be different).

I'm still not sure why they pass for me locally with that error, but I've
updated to patch to match CI.

+ if (host->ssl_ca && host->ssl_ca[0] != '\0')

The comment for HostsLine.ssl_ca, and the code that assigns it,
implies to me that host->ssl_ca should never be NULL. Am I missing a
case where it could be?

The attached version allows ssl_ca to be omitted from the pg_host config to
match the ssl_ca GUC.

On Wed, Dec 3, 2025 at 1:57 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I propose that there is no GUC. In 'pg_hosts.conf', you can specify a
wildcard '*' host that matches anything. You can also specify a "no sni"
line which matches connections with no SNI specified. (Or something
along those lines, I didn't think too hard about all the interactions).

The attached version removes the GUC and instead sets a precedence rule that if
pg_hosts exists and is non-empty, it is exclusively used. If it doesn't exist,
or is empty, then the regular SSL GUCs are used.

Further, pg_hosts is extended with handling * for default fallback, and no_sni
for rules targeting connections with no hostname. The docs changes were harder
than implementing the code, suggestions on how to improve that part would be
greatly appreciated.

But, see below.

Maybe. I'm not a big fan of magic-file-exist configurations

Me neither. (I especially don't like the idea of ignoring a
certificate+key setting that a user has taken the time to put into a
config.)

I wonder if the way forward is to do both? Heikki has a good point that when
working with pg_hosts.conf it should be clear from just that file what the
final config will be, and in the previous version that wasn't the case since
the ssl_snimode GUC set operation modes. At the same time, Jacob has a point
that overriding configuration just because pg_hosts exists isn't transparent.

Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can perhaps
fix both complaints? If the GUC is on, pg_hosts - and only pg_hosts - is used
for configuring secrets. By using the * fallback and no_sni rule in pg_hosts
all variations of configs can be achieved. If the GUC is off, then the regular
SSL GUCs are used and pg_host is never considered (and thus SNI is not
possible).

Such a GUC wouldn't make the patch all that much different from what it is
right now. What do you think about that middleground proposal?

--
Daniel Gustafsson

Attachments:

v13-0001-Serverside-SNI-support-for-libpq.patchapplication/octet-stream; name=v13-0001-Serverside-SNI-support-for-libpq.patch; x-unix-mode=0644Download
From 915e41e110dad5dd9c99e7bbcb629a829d3474b0 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Thu, 11 Dec 2025 18:38:00 +0100
Subject: [PATCH v13] Serverside SNI support for libpq

Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certificate/key handling
is available per host.  A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname.  If pg_hosts.conf is non-empty it will take precedence over
the regular SSL GUCs, if it is empty or missing the regular GUCs will
be used just as before this commit with no hostname specific handling.

Host configuration can either be for a literal hostname to match, non-
SNI connections using the no_sni keyword or a default fallback matching
all connections.  By omitting no_sni and the fallback a strict mode
can be achieved where only connections using sslsni=1 and a specified
hostname are allowed.

CRL file(s) are applied from postgresql.conf to all configured hostnames.

Author: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Dewei Dai <daidewei1970@163.com>
Reviewed-by: Cary Huang <cary.huang@highgo.ca>
Reviewed-by: Heikki Linnakangas <hlinnaka@iki.fi>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 doc/src/sgml/runtime.sgml                     | 118 +++++
 src/backend/Makefile                          |   2 +
 src/backend/libpq/be-secure-common.c          | 198 ++++++++-
 src/backend/libpq/be-secure-openssl.c         | 415 ++++++++++++++++--
 src/backend/libpq/be-secure.c                 |   6 +-
 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     |   7 +
 src/backend/utils/misc/guc_tables.c           |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/bin/initdb/initdb.c                       |  15 +-
 src/include/libpq/hba.h                       |  27 ++
 src/include/libpq/libpq-be.h                  |   3 +-
 src/include/libpq/libpq.h                     |   3 +-
 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/001_ssltests.pl                |   6 +-
 src/test/ssl/t/004_sni.pl                     | 289 ++++++++++++
 src/test/ssl/t/SSL/Backend/OpenSSL.pm         |  16 +-
 src/tools/pgindent/typedefs.list              |   3 +
 23 files changed, 1120 insertions(+), 68 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/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..ca0a114da76 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,118 @@ 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 Server Name
+    Indication, <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 based on
+    the hosts which are defined in <filename>pg_hosts.conf</filename>.
+   </para>
+
+   <para>
+    SNI configuration is defined in the hosts configuration file,
+    <filename>pg_hosts.conf</filename>, which is stored in the cluster's
+    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_CA_certificate</replaceable>,
+    <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>
+    <replaceable>hostname</replaceable> should either be set to the literal
+    hostname for the connection, <literal>no_sni</literal> or <literal>*</literal>.
+    <xref linkend="hostname-values"/> contain details on how these values are
+    used.
+    <table id="hostname-values">
+     <title>Hostname setting values</title>
+     <tgroup cols="3">
+      <thead>
+       <row>
+        <entry>Host Entry</entry>
+        <entry>sslsni</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+
+      <tbody>
+       <row>
+        <entry><literal>*</literal></entry>
+        <entry>Not required</entry>
+        <entry>Default host, matches all connections</entry>
+       </row>
+
+       <row>
+        <entry><literal>no_sni</literal></entry>
+        <entry>Not allowed</entry>
+        <entry>
+         Certificate and key to use for connection with no <literal>sslsni</literal> defined.
+        </entry>
+       </row>
+
+       <row>
+        <entry><replaceable>hostname</replaceable></entry>
+        <entry>Required</entry>
+        <entry>
+         Certificate and key to use for connections to the host specified in the
+         connection.
+        </entry>
+       </row>
+      </tbody>
+
+     </tgroup>
+    </table>
+   </para>
+
+   <para>
+    If <filename>pg_hosts.conf</filename> is empty, or missing, then the SSL
+    configuration in <filename>postgresql.conf</filename> will be used for all
+    connections. If <filename>pg_hosts.conf</filename> is non-empty then it
+    will take precedence over certificate and key settings in
+    <filename>postgresql.conf</filename>.
+   </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>
+
+   <para>
+    The CRL configuration in <filename>postgresql.conf</filename> is applied
+    on all connections regardless of if they use SNI or not.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..529126eebeb 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)
@@ -246,6 +247,7 @@ endif
 	$(MAKE) -C utils uninstall-data
 	rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \
 	      '$(DESTDIR)$(datadir)/pg_ident.conf.sample' \
+	      '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \
 	      '$(DESTDIR)$(datadir)/postgresql.conf.sample'
 ifeq ($(with_llvm), yes)
 	$(call uninstall_llvm_module,postgres)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..be703a87636 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;
+	const char *cmd = (const 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,187 @@ 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 (optional) */
+	field = lnext(tok_line->fields, field);
+	if (!field)
+		return parsedline;
+	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 and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file.  The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false.  This function is
+ * intended to be executed within a temporary memory context which can be
+ * discarded to free memory allocated during the processing of the file.
+ */
+int
+load_hosts(List **hosts, char **err_msg)
+{
+	FILE	   *file;
+	ListCell   *line;
+	List	   *hosts_lines = NIL;
+	List	   *parsed_lines = NIL;
+	HostsLine  *newline;
+	bool		ok = true;
+
+	/*
+	 * If we cannot return results then error out immediately. This implies
+	 * API misuse or a similar kind of programmer error.
+	 */
+	if (!hosts)
+		return HOSTSFILE_LOAD_FAILED;
+	*hosts = NIL;
+
+	/*
+	 * 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, err_msg);
+	if (file == NULL)
+	{
+		if (errno == ENOENT)
+			return HOSTSFILE_MISSING;
+
+		return HOSTSFILE_LOAD_FAILED;
+	}
+
+	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) ||
+			((newline = parse_hosts_line(tok_line, LOG)) == NULL))
+		{
+			ok = false;
+			continue;
+		}
+
+		parsed_lines = lappend(parsed_lines, newline);
+	}
+
+	free_auth_file(file, 0);
+	*hosts = parsed_lines;
+
+	if (!ok)
+		return HOSTSFILE_LOAD_FAILED;
+
+	if (parsed_lines == NIL)
+		return HOSTSFILE_EMPTY;
+
+	return HOSTSFILE_LOAD_OK;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..004a93e4a89 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,15 @@
 #endif
 #include <openssl/x509v3.h>
 
+typedef struct HostContext
+{
+	const char *hostname;
+	SSL_CTX    *context;
+	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 +79,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 +87,26 @@ static const char *SSLerrmessage(unsigned long ecode);
 
 static char *X509_NAME_to_cstring(X509_NAME *name);
 
+/* List of SSL contexts for hostname defined connections */
+static List *sni_contexts = NIL;
+
+/* The default SSL context to use as fallback in case no hostname matches */
+static HostContext *default_context = NULL;
+
+/* The SSL context to use for connections without SNI */
+static HostContext *no_sni_context = NULL;
+
+/* The currently active context */
 static SSL_CTX *SSL_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 +123,181 @@ struct CallbackErr
 
 int
 be_tls_init(bool isServerStart)
+{
+	List	   *pg_hosts = NIL;
+	ListCell   *line;
+	MemoryContext oldcxt;
+	MemoryContext host_memcxt;
+	char	   *err_msg;
+	int			res;
+
+	/*
+	 * 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 (sni_contexts != NIL || default_context || no_sni_context)
+	{
+		Assert(!isServerStart);
+		free_contexts();
+		Host_context = NULL;
+		SSL_context = NULL;
+	}
+
+	/*
+	 * Attempt to load, and parse, TLS configuration from the pg_hosts.conf
+	 * file with the set of hosts returned as a list.  If there are hosts
+	 * configured there they take precedence over the postgresql.conf config.
+	 * Make sure to allocate the parsed rows in a temporary memory context so
+	 * that we can avoid memory leaks from the parsing process.
+	 */
+	host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
+										"hosts file parser context",
+										ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(host_memcxt);
+	res = load_hosts(&pg_hosts, &err_msg);
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * pg_hosts.conf is not required to contain configuration, but if it does
+	 * we error out in case it fails to load rather than continue to try the
+	 * postgresql.conf configuration to avoid silently falling back on an
+	 * undesired configuration.
+	 */
+	if (res == HOSTSFILE_LOAD_FAILED)
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("could not load \"%s\": %s", "pg_hosts.conf",
+					   err_msg ? err_msg : "unknown error"));
+		MemoryContextDelete(host_memcxt);
+		return -1;
+	}
+
+	/*
+	 * Loading and parsing the hosts file was successful, create contexts for
+	 * each host entry and add to the list of hosts to be checked during
+	 * login.
+	 */
+	else if (res == HOSTSFILE_LOAD_OK)
+	{
+		foreach(line, pg_hosts)
+		{
+			HostContext *host_context;
+			HostsLine  *host = lfirst(line);
+			SSL_CTX    *tmp_context = NULL;
+
+			tmp_context = ssl_init_context(isServerStart, host);
+			if (tmp_context == NULL)
+			{
+				ereport(isServerStart ? FATAL : LOG,
+						errcode(ERRCODE_CONFIG_FILE_ERROR),
+						errmsg("unable to load SSL config from \"%s\" line %i",
+							   host->sourcefile, host->linenumber));
+				free_contexts();
+				MemoryContextDelete(host_memcxt);
+				return -1;
+			}
+
+			host_context = palloc0(sizeof(HostContext));
+			host_context->context = tmp_context;
+
+			/* Set flag to remember whether CA store has been loaded */
+			if (host->ssl_ca && host->ssl_ca[0] != '\0')
+				host_context->ssl_loaded_verify_locations = true;
+
+			/*
+			 * The hostname in the context is NULL in case it is the default
+			 * host, or a context to use for non-SNI connections.
+			 */
+			if (strcmp(host->hostname, "*") == 0)
+				default_context = host_context;
+			else if (strcmp(host->hostname, "no_sni") == 0)
+				no_sni_context = host_context;
+			else
+			{
+				host_context->hostname = pstrdup(host->hostname);
+				sni_contexts = lappend(sni_contexts, host_context);
+			}
+
+			/*
+			 * There needs to be an installed context to drive the handshake
+			 * until the SNI callback switches over to the expected one, for
+			 * now just set it to the first one we see.
+			 */
+			if (!Host_context)
+				Host_context = host_context;
+		}
+
+		MemoryContextDelete(host_memcxt);
+	}
+
+	/*
+	 * If the pg_hosts.conf file doesn't exist, or is empty, then load the
+	 * config from postgresql.conf.
+	 */
+	else if (res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
+	{
+		HostsLine	pgconf;
+		SSL_CTX    *tmp_context = NULL;
+
+		memset(&pgconf, 0, sizeof(pgconf));
+		pgconf.ssl_cert = ssl_cert_file;
+		pgconf.ssl_key = ssl_key_file;
+		pgconf.ssl_ca = ssl_ca_file;
+		pgconf.ssl_passphrase_cmd = ssl_passphrase_command;
+		pgconf.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+		tmp_context = ssl_init_context(isServerStart, &pgconf);
+		if (tmp_context == NULL)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("could not load SSL configuration from \"%s\"",
+						   "postgresql.conf"));
+			return -1;
+		}
+
+		/*
+		 * If postgresql.conf is used to configure SSL then by definition it
+		 * will be the default context as we don't have per-host config.  We
+		 * can also set it as the Host_context since it will be used for all
+		 * connections.
+		 */
+		default_context = palloc0(sizeof(HostContext));
+		default_context->context = tmp_context;
+		Host_context = default_context;
+
+		/* Set flag to remember whether CA store has been loaded */
+		if (ssl_ca_file[0])
+			default_context->ssl_loaded_verify_locations = true;
+	}
+
+	/* Make sure we have at least one certificate loaded */
+	if (sni_contexts == NIL && !default_context && !no_sni_context)
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("no SSL contexts loaded"));
+		return -1;
+	}
+
+	SSL_context = Host_context->context;
+
+	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 +323,16 @@ be_tls_init(bool isServerStart)
 	 */
 	SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
 
+	/*
+	 * Install SNI TLS extension callback in order to validate hostnames in
+	 * case we have at least one context configured with a host name.
+	 */
+	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 +340,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 +358,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 +522,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 && 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;
 		}
 
@@ -347,18 +544,17 @@ be_tls_init(bool isServerStart)
 		 * free it when no longer needed.
 		 */
 		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.
-		 */
-		SSL_CTX_set_verify(context,
-						   (SSL_VERIFY_PEER |
-							SSL_VERIFY_CLIENT_ONCE),
-						   verify_cb);
 	}
 
+	/*
+	 * 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.
+	 */
+	SSL_CTX_set_verify(context,
+					   (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+					   verify_cb);
+
 	/*----------
 	 * Load the Certificate Revocation List (CRL).
 	 * http://searchsecurity.techtarget.com/sDefinition/0,,sid14_gci803160,00.html
@@ -407,38 +603,19 @@ 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);
-	SSL_context = NULL;
-	ssl_loaded_verify_locations = false;
+	free_contexts();
 }
 
 int
@@ -771,6 +948,9 @@ be_tls_close(Port *port)
 		pfree(port->peer_dn);
 		port->peer_dn = NULL;
 	}
+
+	Host_context = NULL;
+	SSL_context = NULL;
 }
 
 ssize_t
@@ -1144,7 +1324,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 +1570,92 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames.  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;
+	HostContext *install_context = NULL;
+
+	tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+	/*
+	 * If there is no hostname set in the TLS extension, we have two options:
+	 * i) there is a HostContext defined for non-SNI connections, in that case
+	 * we switch to that; ii) there is no non-SNI config and we error out as
+	 * there is no context to switch to.
+	 */
+	if (!tlsext_hostname)
+	{
+		if (no_sni_context)
+			install_context = no_sni_context;
+		else if (default_context)
+			install_context = default_context;
+		else
+		{
+			/*
+			 * 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
+	{
+		/*
+		 * We have a requested hostname from the client, match against all
+		 * entries in the pg_hosts configuration and attempt to find a match.
+		 */
+		foreach_ptr(HostContext, host, sni_contexts)
+		{
+			if (strcmp(host->hostname, tlsext_hostname) == 0)
+			{
+				install_context = host;
+				break;
+			}
+		}
+
+		/*
+		 * If no host specific match was found, and there is a default config,
+		 * then fall back to using that.
+		 */
+		if (!install_context && default_context)
+			install_context = default_context;
+	}
+
+	/*
+	 * If we reach here without a context chosen as the session context then
+	 * fail the handshake and terminate the connection.
+	 */
+	if (install_context == NULL)
+		return SSL_TLSEXT_ERR_ALERT_FATAL;
+
+	Host_context = install_context;
+	SSL_context = install_context->context;
+	if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+	{
+		ereport(COMMERROR,
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("failed to switch to SSL context for host"));
+		return SSL_TLSEXT_ERR_ALERT_FATAL;
+	}
+
+	return SSL_TLSEXT_ERR_OK;
+}
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1599,6 +1865,14 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
 		ptr[0] = '\0';
 }
 
+bool
+be_tls_loaded_verify_locations(void)
+{
+	if (!Host_context)
+		return false;
+	return Host_context->ssl_loaded_verify_locations;
+}
+
 char *
 be_tls_get_certificate_hash(Port *port, size_t *len)
 {
@@ -1792,17 +2066,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 +2094,42 @@ 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 (sni_contexts != NIL)
+	{
+		foreach_ptr(HostContext, host, sni_contexts)
+		{
+			if (host->hostname)
+				pfree(unconstify(char *, host->hostname));
+			SSL_CTX_free(host->context);
+		}
+
+		list_free_deep(sni_contexts);
+		sni_contexts = NIL;
+	}
+
+	/*
+	 * The hostname need not be freed for the no_sni and default contexts
+	 * since they by definition are not connected to a hostname and thus have
+	 * none allocated.
+	 */
+	if (no_sni_context)
+	{
+		SSL_CTX_free(no_sni_context->context);
+		pfree(no_sni_context);
+		no_sni_context = NULL;
+	}
+	if (default_context)
+	{
+		SSL_CTX_free(default_context->context);
+		pfree(default_context);
+		default_context = NULL;
+	}
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..2e6be47887c 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;
@@ -99,7 +95,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..a31c49b01f7
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME       SSL CERTIFICATE             SSL KEY     SSL CA       PASSPHRASE COMMAND         PASSPHRASE COMMAND RELOAD
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 935c235e1b3..2e30e564715 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 3b9d8349078..4ce670e8347 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1167,6 +1167,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',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..1d26628f879 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -556,6 +556,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..1f360110564 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
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 7b93ba4a709..38713381255 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,33 @@ 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;
+
+typedef enum HostsFileLoad
+{
+	HOSTSFILE_LOAD_OK = 0,
+	HOSTSFILE_LOAD_FAILED,
+	HOSTSFILE_EMPTY,
+	HOSTSFILE_MISSING,
+} HostsFileLoadResult;
+
 /*
  * 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..b9b2c8bd5af 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -156,8 +156,9 @@ enum ssl_protocol_versions
  * 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 int	load_hosts(List **hosts, char **err_msg);
 
 #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 295988b8b87..11f9280b341 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 against 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/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index fc7c35ef879..15ca0a0e8c2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -380,11 +380,11 @@ switch_server_cert($node, certfile => 'server-ip-cn-only');
 $common_connstr =
   "$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
 
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
 	"IP address in the Common Name");
 
 $node->connect_fails(
-	"$common_connstr host=192.000.002.001",
+	"$common_connstr host=192.000.002.001 sslsni=0",
 	"mismatch between host name and server certificate IP address",
 	expected_stderr =>
 	  qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
@@ -394,7 +394,7 @@ $node->connect_fails(
 # long-standing behavior.)
 switch_server_cert($node, certfile => 'server-ip-in-dnsname');
 
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
 	"IP address in a dNSName");
 
 # Test Subject Alternative Names.
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..2dd70e7afee
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,289 @@
+
+# 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 hostaddr 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 sslsni=1";
+
+##############################################################################
+# postgresql.conf
+##############################################################################
+
+# Connect without any hosts configured in pg_hosts.conf, thus using the cert
+# and key in postgresql.conf. pg_hosts.conf exists at this point but is empty
+# apart from the comments stemming from the sample.
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"pg.conf: connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	"pg.conf: connect fails without intermediate for sslmode=verify-ca",
+	expected_stderr => qr/certificate verify failed/);
+
+# Remove pg_hosts.conf and reload to make sure a missing file is treated like
+# an empty file.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"pg.conf: connect after deleting pg_hosts.conf");
+
+##############################################################################
+# pg_hosts.conf
+##############################################################################
+
+# Replicate the postgresql.conf configuration into pg_hosts.conf and retry the
+# same tests as above.
+$node->append_conf('pg_hosts.conf',
+	"* server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"pg_hosts.conf: connect to default, with correct server CA cert file sslmode=require"
+);
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	"pg_hosts.conf: connect to default, fail without intermediate for sslmode=verify-ca",
+	expected_stderr => qr/certificate verify failed/);
+
+# Add host entry for example.org which serves the server cert and its
+# intermediate CA.  The previously existing default host still exists without
+# a 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",
+	"pg_hosts.conf: connect to example.org and verify server CA");
+
+$node->connect_fails(
+	"$connstr host=example.org sslrootcert=invalid sslmode=verify-ca",
+	"pg_hosts.conf: connect to example.org but 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",
+	"pg_hosts.conf: connect to default and fail to verify CA",
+	expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"pg_hosts.conf: connect to default with sslmode=require");
+
+# Modify pg_hosts.conf to no longer have the default host entry.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	"example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+# Connecting without a hostname as well as with a hostname which isn't in the
+# pg_hosts configuration should fail.
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+	"pg_hosts.conf: connect to default with sslmode=require",
+	expected_stderr => qr/missing extension/);
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.com",
+	"pg_hosts.conf: connect to default with sslmode=require",
+	expected_stderr => qr/unrecognized name/);
+
+# 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,
+	'pg_hosts.conf: 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,
+	'pg_hosts.conf: 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 host=localhost",
+	"pg_hosts.conf: 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 host=localhost",
+	"pg_hosts.conf: 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 host=localhost",
+	"pg_hosts.conf: 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,
+	'pg_hosts.conf: 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 host=localhost",
+		"pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+	);
+}
+
+$node->reload;
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+	"pg_hosts.conf: connect fails since the passphrase protected key cannot be reloaded"
+);
+
+# Configure with only non-SNI connections allowed
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	"no_sni server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+	"pg_hosts.conf: only non-SNI connections allowed");
+
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.org",
+	"pg_hosts.conf: only non-SNI connections allowed, connecting with SNI",
+	expected_stderr => qr/unrecognized name/);
+
+# 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/unknown ca/);
+
+	# 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/connection requires a valid client certificate/
+	);
+
+	$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/connection requires a valid client certificate/
+	);
+
+	$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/unknown ca/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 4159addb700..bbd3bed6c86 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 9dd65b10254..43bd1ac3f99 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1209,6 +1209,9 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostContext
+HostsFileLoadResult
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

#37Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Daniel Gustafsson (#36)
Re: Serverside SNI support in libpq

On 12/12/2025 13:41, Daniel Gustafsson wrote:

On Wed, Dec 3, 2025 at 1:57 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:

Maybe. I'm not a big fan of magic-file-exist configurations

Me neither. (I especially don't like the idea of ignoring a
certificate+key setting that a user has taken the time to put into a
config.)

+1

I wonder if the way forward is to do both? Heikki has a good point that when
working with pg_hosts.conf it should be clear from just that file what the
final config will be, and in the previous version that wasn't the case since
the ssl_snimode GUC set operation modes. At the same time, Jacob has a point
that overriding configuration just because pg_hosts exists isn't transparent.

Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can perhaps
fix both complaints? If the GUC is on, pg_hosts - and only pg_hosts - is used
for configuring secrets. By using the * fallback and no_sni rule in pg_hosts
all variations of configs can be achieved. If the GUC is off, then the regular
SSL GUCs are used and pg_host is never considered (and thus SNI is not
possible).

Such a GUC wouldn't make the patch all that much different from what it is
right now. What do you think about that middleground proposal?

I like that.

Instead of a boolean GUC, it could perhaps be a path to the pg_hosts
file. I haven't thought this through but somehow it feels more natural
to me than a "read this file or not" setting.

- Heikki

#38Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Heikki Linnakangas (#37)
Re: Serverside SNI support in libpq

On 17/12/2025 11:03, Heikki Linnakangas wrote:

On 12/12/2025 13:41, Daniel Gustafsson wrote:

I wonder if the way forward is to do both?  Heikki has a good point
that when
working with pg_hosts.conf it should be clear from just that file what
the
final config will be, and in the previous version that wasn't the case
since
the ssl_snimode GUC set operation modes.  At the same time, Jacob has
a point
that overriding configuration just because pg_hosts exists isn't
transparent.

Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can
perhaps
fix both complaints?  If the GUC is on, pg_hosts - and only pg_hosts -
is used
for configuring secrets.  By using the * fallback and no_sni rule in
pg_hosts
all variations of configs can be achieved.  If the GUC is off, then
the regular
SSL GUCs are used and pg_host is never considered (and thus SNI is not
possible).

Such a GUC wouldn't make the patch all that much different from what
it is
right now. What do you think about that middleground proposal?

I like that.

Instead of a boolean GUC, it could perhaps be a path to the pg_hosts
file. I haven't thought this through but somehow it feels more natural
to me than a "read this file or not" setting.

I was thinking that the boolean GUC would be called something like
"read_pg_hosts_file = on / off", which feels unnatural. But thinking
about this more, if the GUC is called something like "enable_sni = on /
off", that feels much better, and I like that more than my suggestion of
specifying the path to the pg_hosts file.

- Heikki

#39Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#36)
Re: Serverside SNI support in libpq

On Fri, Dec 12, 2025 at 3:41 AM Daniel Gustafsson <daniel@yesql.se> wrote:

The comment for HostsLine.ssl_ca, and the code that assigns it,
implies to me that host->ssl_ca should never be NULL. Am I missing a
case where it could be?

The attached version allows ssl_ca to be omitted from the pg_host config to
match the ssl_ca GUC.

Aha! I think ssl_ca should be moved into the "Optional fields" section
of `struct HostsLine` now.

I'm still not sure why they pass for me locally with that error, but I've
updated to patch to match CI.

There's one diff remaining from my old tests patch: the example.org
line doesn't set ssl_ca, so I expect

-       expected_stderr => qr/unknown ca/);
+       expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);

because host_context->ssl_loaded_verify_locations should be false. But
that doesn't happen... Why?

Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can perhaps
fix both complaints?

Sounds reasonable, I think.

--

Just checking my understanding: is the use case for no_sni primarily
that you should be able to strictly refuse clients who say they're
talking to someone else -- so you don't want a wildcard -- but you
still want to gracefully handle clients who don't speak SNI at all?

+           else if (strcmp(host->hostname, "no_sni") == 0)
+               no_sni_context = host_context;

Will anyone be mad at us for camping on the "no_sni" identifier? I
know technically underscore isn't allowed in DNS hostnames, buuuut [1,
2]

+   /* Hostname */
+   field = list_head(tok_line->fields);
+   tokens = lfirst(field);
+   token = linitial(tokens);
+   parsedline->hostname = pstrdup(token->string);

We should probably check tokens->length to make sure that the user
hasn't passed more than one token for each field, similar to how
parse_hba_line() does it.

Should we support multiple hostname tokens in a single line, though,
and just copy the settings that follow across all of them? That would
allow you to collapse

example.org server.crt server.key
example.com server.crt server.key
sub.example.com server.crt server.key
* other.crt other.key

into

example.org,example.com,sub.example.com server.crt server.key
* other.crt other.key

or even

@my-hostnames.txt server.crt server.key
* other.crt other.key

Then you'd have a fighting chance at automatically generating the
lists, especially since we don't do wildcards yet.

--Jacob

[1]: https://github.com/netty/netty/pull/8150
[2]: https://github.com/openssl/openssl/issues/12566

#40Daniel Gustafsson
daniel@yesql.se
In reply to: Jacob Champion (#39)
Re: Serverside SNI support in libpq

On 18 Dec 2025, at 00:58, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

On Fri, Dec 12, 2025 at 3:41 AM Daniel Gustafsson <daniel@yesql.se> wrote:

The comment for HostsLine.ssl_ca, and the code that assigns it,
implies to me that host->ssl_ca should never be NULL. Am I missing a
case where it could be?

The attached version allows ssl_ca to be omitted from the pg_host config to
match the ssl_ca GUC.

Aha! I think ssl_ca should be moved into the "Optional fields" section
of `struct HostsLine` now.

Ah, yes.

I'm still not sure why they pass for me locally with that error, but I've
updated to patch to match CI.

There's one diff remaining from my old tests patch: the example.org
line doesn't set ssl_ca, so I expect

-       expected_stderr => qr/unknown ca/);
+       expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);

because host_context->ssl_loaded_verify_locations should be false. But
that doesn't happen... Why?

I'll have a look.

Just checking my understanding: is the use case for no_sni primarily
that you should be able to strictly refuse clients who say they're
talking to someone else -- so you don't want a wildcard -- but you
still want to gracefully handle clients who don't speak SNI at all?

Yeah, pretty much.

+           else if (strcmp(host->hostname, "no_sni") == 0)
+               no_sni_context = host_context;

Will anyone be mad at us for camping on the "no_sni" identifier? I
know technically underscore isn't allowed in DNS hostnames, buuuut [1,
2]

Maybe, but I think that regardless of what we do someone will be mad. The
other option would be to use another single character like '?' or something.
Not sure that will improve readability though.

+   /* Hostname */
+   field = list_head(tok_line->fields);
+   tokens = lfirst(field);
+   token = linitial(tokens);
+   parsedline->hostname = pstrdup(token->string);

We should probably check tokens->length to make sure that the user
hasn't passed more than one token for each field, similar to how
parse_hba_line() does it.

Good point, will do that.

Should we support multiple hostname tokens in a single line, though,
and just copy the settings that follow across all of them?

I've been hesitant to add too much complexity, but perhaps just allowing a
comma separated list is a good middle ground to avoid going full regex?

--
Daniel Gustafsson

#41Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#40)
Re: Serverside SNI support in libpq

On Wed, Dec 17, 2025 at 4:07 PM Daniel Gustafsson <daniel@yesql.se> wrote:

Will anyone be mad at us for camping on the "no_sni" identifier? I
know technically underscore isn't allowed in DNS hostnames, buuuut [1,
2]

Maybe, but I think that regardless of what we do someone will be mad. The
other option would be to use another single character like '?' or something.
Not sure that will improve readability though.

Hm, I agree that's not readable. Especially since other famous server
implementations use ? to match a single character in server alias
names.

Maybe we could enclose no_sni with something that's emphatically not
DNS. Braces, brackets, etc.? If we had control over the lower level
tokenizer, we could tell people to double-quote it to disambiguate,
but I don't think we have access to that information at our level.

Should we support multiple hostname tokens in a single line, though,
and just copy the settings that follow across all of them?

I've been hesitant to add too much complexity, but perhaps just allowing a
comma separated list is a good middle ground to avoid going full regex?

I think it could be a pretty good bump in usability. Wildcards seem
ideal but the cost is much higher. Hopefully the cost of
comma-separated hosts is just an extra inner loop in the parser, plus
the extra tests?

I'm trying to put on my "what could we possibly regret" hat for these
next ones. They may be uselessly speculative:

- If the goal is to eventually support wildcards, will the use of a
bare catch-all asterisk conflict with your plans (if any)?
- What kind of normalization should we do? Currently, `example.com`
will not match `example.COM` and it seems like that might be a problem
for somebody.
- Do we need to consider IDNs and A-labels and U-labels? (Do we
support the latter today, at all?)

A nice-to-have v2ish feature might be to warn if the host configured
for a certificate cannot in fact match that certificate according to
OpenSSL.

--Jacob

#42Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#41)
Re: Serverside SNI support in libpq

On Thu, Dec 18, 2025 at 9:06 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

A nice-to-have v2ish feature might be to warn if the host configured
for a certificate cannot in fact match that certificate according to
OpenSSL.

Another wishlist item: the logs (both server- and client-side) are
pretty inscrutable when things fail right now. Server's relatively
easy to change, but I wonder if we can do something along the lines of
0b5d1fb36 to provide an extra hint on the client side?

--Jacob