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)

