From 4d5253b0cf1d0a981ee4ac1b750310c7c70197b5 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 6 Mar 2026 23:00:51 +0100
Subject: [PATCH v15 2/2] ssl: 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.  In order to use SNI the ssl_sni GUC must be set to on, when
it is off the ssl configuration works just like before.  If ssl_sni is
enabled and 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.

Serverside SNI requires OpenSSL, currently LibreSSL does not support
the required infrastructure to update the SSL context during the TLS
handshake.

Author: Daniel Gustafsson <daniel@yesql.se>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
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
---
 configure                                     |   2 +-
 configure.ac                                  |   2 +-
 doc/src/sgml/runtime.sgml                     | 123 +++
 meson.build                                   |   1 +
 src/backend/Makefile                          |   2 +
 src/backend/commands/variable.c               |  21 +
 src/backend/libpq/be-secure-common.c          | 258 +++++-
 src/backend/libpq/be-secure-openssl.c         | 785 ++++++++++++++++--
 src/backend/libpq/be-secure.c                 |   3 +
 src/backend/libpq/meson.build                 |   1 +
 src/backend/libpq/pg_hosts.conf.sample        |   4 +
 src/backend/utils/misc/guc.c                  |  32 +
 src/backend/utils/misc/guc_parameters.dat     |  15 +
 src/backend/utils/misc/guc_tables.c           |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   3 +
 src/bin/initdb/initdb.c                       |  15 +-
 src/include/libpq/hba.h                       |  30 +
 src/include/libpq/libpq.h                     |   5 +-
 src/include/pg_config.h.in                    |   3 +
 src/include/utils/guc.h                       |   1 +
 src/include/utils/guc_hooks.h                 |   1 +
 src/test/perl/PostgreSQL/Test/Cluster.pm      |  35 +
 src/test/ssl/meson.build                      |   1 +
 src/test/ssl/t/001_ssltests.pl                |  23 +-
 src/test/ssl/t/004_sni.pl                     | 396 +++++++++
 src/tools/pgindent/typedefs.list              |   2 +
 26 files changed, 1663 insertions(+), 102 deletions(-)
 create mode 100644 src/backend/libpq/pg_hosts.conf.sample
 create mode 100644 src/test/ssl/t/004_sni.pl

diff --git a/configure b/configure
index 4aaaf92ba0a..cd10d983fe5 100755
--- a/configure
+++ b/configure
@@ -13200,7 +13200,7 @@ fi
 done
 
   # Function introduced in OpenSSL 1.1.1, not in LibreSSL.
-  for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback
+  for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb
 do :
   as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
 ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
diff --git a/configure.ac b/configure.ac
index 9bc457bac87..6a1a30298e4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1450,7 +1450,7 @@ if test "$with_ssl" = openssl ; then
   # Function introduced in OpenSSL 1.0.2, not in LibreSSL.
   AC_CHECK_FUNCS([SSL_CTX_set_cert_cb])
   # Function introduced in OpenSSL 1.1.1, not in LibreSSL.
-  AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback])
+  AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb])
   AC_DEFINE([USE_OPENSSL], 1, [Define to 1 to build with OpenSSL support. (--with-ssl=openssl)])
 elif test "$with_ssl" != no ; then
   AC_MSG_ERROR([--with-ssl must specify openssl])
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index b1937cd13ab..cf2e7302ddb 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2469,6 +2469,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>
@@ -2596,6 +2602,123 @@ 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>
+<literal>include</literal> <replaceable>file</replaceable>
+<literal>include_if_exists</literal> <replaceable>file</replaceable>
+<literal>include_dir</literal> <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"/> contains 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 connections 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.  Multiple hostnames can be defined by using a comma
+         separated list. The certificate and key will be used for connections
+         to all hosts in the list.
+        </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/meson.build b/meson.build
index 2df54409ca6..e4804badb91 100644
--- a/meson.build
+++ b/meson.build
@@ -1674,6 +1674,7 @@ if sslopt in ['auto', 'openssl']
       ['X509_get_signature_info'],
       ['SSL_CTX_set_num_tickets'],
       ['SSL_CTX_set_keylog_callback'],
+      ['SSL_CTX_set_client_hello_cb'],
     ]
 
     are_openssl_funcs_complete = true
diff --git a/src/backend/Makefile b/src/backend/Makefile
index ba53cd9d998..162d3f1f2a9 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -221,6 +221,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)
@@ -280,6 +281,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/commands/variable.c b/src/backend/commands/variable.c
index 4440aff4925..8afd252fc8c 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -1258,6 +1258,27 @@ check_ssl(bool *newval, void **extra, GucSource source)
 	return true;
 }
 
+bool
+check_ssl_sni(bool *newval, void **extra, GucSource source)
+{
+#ifndef USE_SSL
+	if (*newval)
+	{
+		GUC_check_errmsg("SSL is not supported by this build");
+		return false;
+	}
+#else
+#ifndef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+	if (*newval)
+	{
+		GUC_check_errmsg("SNI requires OpenSSL 1.1.1 or later");
+		return false;
+	}
+#endif
+#endif
+	return true;
+}
+
 bool
 check_standard_conforming_strings(bool *newval, void **extra, GucSource source)
 {
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index c074556dbfc..6b61fb59ba1 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -26,18 +26,25 @@
 #include "common/string.h"
 #include "libpq/libpq.h"
 #include "storage/fd.h"
+#include "utils/builtins.h"
+#include "utils/guc.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 *cmd, const char *prompt,
+						   bool is_server_start, char *buf, int size)
 {
 	int			loglevel = is_server_start ? ERROR : LOG;
 	char	   *command;
@@ -49,7 +56,7 @@ run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf,
 	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 +182,248 @@ 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);
+	parsedline->hostnames = NIL;
+
+	/* Initialize optional fields */
+	parsedline->ssl_passphrase_cmd = NULL;
+	parsedline->ssl_passphrase_reload = false;
+
+	/* Hostname */
+	field = list_head(tok_line->fields);
+	tokens = lfirst(field);
+	foreach_ptr(AuthToken, hostname, tokens)
+	{
+		if ((tokens->length > 1) &&
+			(strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0))
+		{
+			ereport(elevel,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					errmsg("default and non-SNI entries cannot be mixed with other entries"),
+					errcontext("line %d of configuration file \"%s\"",
+							   tok_line->line_num, tok_line->file_name));
+			return NULL;
+		}
+
+		parsedline->hostnames = lappend(parsedline->hostnames, pstrdup(hostname->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);
+	if (tokens->length > 1)
+	{
+		ereport(elevel,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("multiple values specified for SSL certificate"),
+				errcontext("line %d of configuration file \"%s\"",
+						   tok_line->line_num, tok_line->file_name));
+		return NULL;
+	}
+	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);
+	if (tokens->length > 1)
+	{
+		ereport(elevel,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("multiple values specified for SSL key"),
+				errcontext("line %d of configuration file \"%s\"",
+						   tok_line->line_num, tok_line->file_name));
+		return NULL;
+	}
+	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);
+	if (tokens->length > 1)
+	{
+		ereport(elevel,
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("multiple values specified for SSL CA"),
+				errcontext("line %d of configuration file \"%s\"",
+						   tok_line->line_num, tok_line->file_name));
+		return NULL;
+	}
+	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);
+
+			/*
+			 * There should be no more tokens after this, if there are break
+			 * parsing and report error to avoid silently accepting incorrect
+			 * config.
+			 */
+			if (tokens->length > 1)
+			{
+				ereport(elevel,
+						errcode(ERRCODE_CONFIG_FILE_ERROR),
+						errmsg("extra fields at end of line"),
+						errcontext("line %d of configuration file \"%s\"",
+								   tok_line->line_num, tok_line->file_name));
+				return NULL;
+			}
+
+			if (!parse_bool(token->string, &parsedline->ssl_passphrase_reload))
+			{
+				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 HostsLine elements containing the parsed lines, or NIL in case of an empty
+ * file.  The list is returned in the hosts parameter. The function will return
+ * a HostsFileLoadResult value detailing the result of the operation.  When
+ * the hosts configuration failed to load, the err_msg variable may have more
+ * information in case it was passed as non-NULL.
+ */
+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)
+	{
+		if (err_msg)
+			*err_msg = psprintf("cannot load config from \"%s\", return variable missing",
+								HostsFileName);
+		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);
+
+		/*
+		 * Mark processing as not-ok in case lines are found with errors in
+		 * tokenization (.err_msg is set) or during parsing.
+		 */
+		if ((tok_line->err_msg != NULL) ||
+			((newline = parse_hosts_line(tok_line, LOG)) == NULL))
+		{
+			ok = false;
+			continue;
+		}
+
+		parsed_lines = lappend(parsed_lines, newline);
+	}
+
+	/* Free memory from tokenizer */
+	free_auth_file(file, 0);
+	*hosts = parsed_lines;
+
+	if (!ok)
+	{
+		if (err_msg)
+			*err_msg = psprintf("loading config from \"%s\" failed due to parsing error",
+								HostsFileName);
+		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 14c6532bb16..a191367c9b2 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -78,10 +78,34 @@ 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);
 static const char *SSLerrmessage(unsigned long ecode);
+static bool init_host_context(HostsLine *host, bool isServerStart);
+static void host_context_cleanup_cb(void *arg);
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+static int	sni_clienthello_cb(SSL *ssl, int *al, void *arg);
+#endif
 
 static char *X509_NAME_to_cstring(X509_NAME *name);
 
 static SSL_CTX *SSL_context = NULL;
+static MemoryContext SSL_hosts_memcxt = NULL;
+static struct hosts
+{
+	/*
+	 * List of HostsLine structures containing SSL configurations for
+	 * connections with hostnames defined in the SNI extension.
+	 */
+	List	   *sni;
+
+	/* The SSL configuration to use for connections without SNI */
+	HostsLine  *no_sni;
+
+	/*
+	 * The default SSL configuration to use as a fallback in case no hostname
+	 * matches the supplied hostname in the SNI extension.
+	 */
+	HostsLine  *default_host;
+}		   *SSL_hosts;
+
 static bool dummy_ssl_passwd_cb_called = false;
 static bool ssl_is_server_start;
 
@@ -104,88 +128,244 @@ struct CallbackErr
 int
 be_tls_init(bool isServerStart)
 {
-	SSL_CTX    *context;
+	List	   *pg_hosts = NIL;
+	ListCell   *line;
+	MemoryContext oldcxt;
+	MemoryContext host_memcxt = NULL;
+	MemoryContextCallback *host_memcxt_cb;
+	char	   *err_msg = NULL;
+	int			res;
+	struct hosts *new_hosts;
+	SSL_CTX    *context = NULL;
 	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
-	 * freeing this context; we don't install it as active until the end.
+	 * Since we don't know which host we're using until the ClientHello is
+	 * sent, ssl_loaded_verify_locations *always* starts out as false. The
+	 * only place it's set to true is in sni_clienthello_cb().
+	 */
+	ssl_loaded_verify_locations = false;
+
+	host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
+										"hosts file parser context",
+										ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(host_memcxt);
+
+	/* Allocate a tentative replacement for SSL_hosts. */
+	new_hosts = palloc0_object(struct hosts);
+
+	/*
+	 * Register a reset callback for the memory context which is responsible
+	 * for freeing OpenSSL managed allocations upon context deletion.  The
+	 * callback is allocated here to make sure it gets cleaned up along with
+	 * the memory context it's registered for.
+	 */
+	host_memcxt_cb = palloc0_object(MemoryContextCallback);
+	host_memcxt_cb->func = host_context_cleanup_cb;
+	host_memcxt_cb->arg = new_hosts;
+	MemoryContextRegisterResetCallback(host_memcxt, host_memcxt_cb);
+
+	/*
+	 * If ssl_sni is enabled, 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 they take precedence over the configuration
+	 * in postgresql.conf.  Make sure to allocate the parsed rows in their own
+	 * memory context so that we can delete them easily in case parsing fails.
+	 * If ssl_sni is disabled then set the state accordingly to make sure we
+	 * instead parse the config from postgresql.conf.
 	 *
-	 * We use SSLv23_method() because it can negotiate use of the highest
-	 * mutually supported protocol version, while alternatives like
-	 * TLSv1_2_method() permit only one specific version.  Note that we don't
-	 * actually allow SSL v2 or v3, only TLS protocols (see below).
+	 * The reason for not doing everything in this if-else conditional is that
+	 * we want to use the same processing of postgresql.conf for when ssl_sni
+	 * is off as well as when it's on but the hostsfile is missing etc.  Thus
+	 * we set res to the state and continue with a new conditional instead of
+	 * duplicating logic and risk it diverging over time.
 	 */
-	context = SSL_CTX_new(SSLv23_method());
-	if (!context)
+	if (ssl_sni)
 	{
+		/*
+		 * The GUC check hook should have already blocked this but to be on
+		 * the safe side we doublecheck here.
+		 */
+#ifndef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
 		ereport(isServerStart ? FATAL : LOG,
-				(errmsg("could not create SSL context: %s",
-						SSLerrmessage(ERR_get_error()))));
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("ssl_sni is not supported with LibreSSL"));
 		goto error;
+#endif
+
+		/* Attempt to load configuration from pg_hosts.conf */
+		res = load_hosts(&pg_hosts, &err_msg);
+
+		/*
+		 * 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"));
+			goto error;
+		}
 	}
+	else
+		res = HOSTSFILE_DISABLED;
 
 	/*
-	 * Disable OpenSSL's moving-write-buffer sanity check, because it causes
-	 * unnecessary failures in nonblocking send cases.
+	 * Loading and parsing the hosts file was successful, create configs for
+	 * each host entry and add to the list of hosts to be checked during
+	 * login.
 	 */
-	SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+	if (res == HOSTSFILE_LOAD_OK)
+	{
+		Assert(ssl_sni);
+
+		foreach(line, pg_hosts)
+		{
+			HostsLine  *host = lfirst(line);
+
+			if (!init_host_context(host, isServerStart))
+				goto error;
+
+			/*
+			 * The hostname in the config will be set to NULL for the default
+			 * host as well as in configs used for non-SNI connections.  Lists
+			 * of hostnames in pg_hosts.conf are not allowed to contain the
+			 * default '*' entry or a '/no_sni/' entry and this is checked
+			 * during parsing.  Thus we can inspect the head of the hostnames
+			 * list for these since they will never be anywhere else.
+			 */
+			if (strcmp(linitial(host->hostnames), "*") == 0)
+			{
+				if (new_hosts->default_host)
+				{
+					ereport(isServerStart ? FATAL : LOG,
+							errcode(ERRCODE_CONFIG_FILE_ERROR),
+							errmsg("multiple default hosts specified"),
+							errcontext("line %d of configuration file \"%s\"",
+									   host->linenumber, host->sourcefile));
+					goto error;
+				}
+
+				new_hosts->default_host = host;
+			}
+			else if (strcmp(linitial(host->hostnames), "/no_sni/") == 0)
+			{
+				if (new_hosts->no_sni)
+				{
+					ereport(isServerStart ? FATAL : LOG,
+							errcode(ERRCODE_CONFIG_FILE_ERROR),
+							errmsg("multiple no_sni hosts specified"),
+							errcontext("line %d of configuration file \"%s\"",
+									   host->linenumber, host->sourcefile));
+					goto error;
+				}
+
+				new_hosts->no_sni = host;
+			}
+			else
+			{
+				/*
+				 * At this point we know we have a configuration with a list
+				 * of 1..n hostnames for literal string matching with the SNI
+				 * extension from the user.
+				 */
+				new_hosts->sni = lappend(new_hosts->sni, host);
+			}
+		}
+	}
 
 	/*
-	 * Call init hook (usually to set password callback)
+	 * If SNI is disabled, then we load configuration from postgresql.conf. If
+	 * SNI is enabled but the pg_hosts.conf file doesn't exist, or is empty,
+	 * then we also load the config from postgresql.conf.
 	 */
-	(*openssl_tls_init_hook) (context, isServerStart);
+	else if (res == HOSTSFILE_DISABLED || res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
+	{
+		HostsLine  *pgconf = palloc0(sizeof(HostsLine));
 
-	/* used by the callback */
-	ssl_is_server_start = isServerStart;
+#ifdef USE_ASSERT_CHECKING
+		if (res == HOSTSFILE_DISABLED)
+			Assert(ssl_sni == false);
+#endif
+
+		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;
+
+		if (!init_host_context(pgconf, isServerStart))
+			goto error;
+
+		/*
+		 * 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.
+		 */
+		new_hosts->default_host = pgconf;
+	}
 
 	/*
-	 * Load and verify server's certificate and private key
+	 * Make sure we have at least one configuration loaded to use, without
+	 * that we cannot drive a connection so exit.
 	 */
-	if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+	if (new_hosts->sni == NIL && !new_hosts->default_host && !new_hosts->no_sni)
 	{
 		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()))));
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+				errmsg("no SSL configurations loaded"),
+		/*- translator: The two %s contain filenames */
+				errhint("If ssl_sni is enabled then add configuration to \"%s\", else \"%s\"",
+						"pg_hosts.conf", "postgresql.conf"));
 		goto error;
 	}
 
-	if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
-		goto error;
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
 
 	/*
-	 * OK, try to load the private key file.
+	 * 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
+	 * freeing this context; we don't install it as active until the end.
+	 *
+	 * We use SSLv23_method() because it can negotiate use of the highest
+	 * mutually supported protocol version, while alternatives like
+	 * TLSv1_2_method() permit only one specific version.  Note that we don't
+	 * actually allow SSL v2 or v3, only TLS protocols (see below).
 	 */
-	dummy_ssl_passwd_cb_called = false;
-
-	if (SSL_CTX_use_PrivateKey_file(context,
-									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)));
-		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()))));
-		goto error;
-	}
-
-	if (SSL_CTX_check_private_key(context) != 1)
+	context = SSL_CTX_new(SSLv23_method());
+	if (!context)
 	{
 		ereport(isServerStart ? FATAL : LOG,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("check of private key failed: %s",
+				(errmsg("could not create SSL context: %s",
 						SSLerrmessage(ERR_get_error()))));
 		goto error;
 	}
+#else
+
+	/*
+	 * If the client hello callback isn't supported we want to use the default
+	 * context as the one to drive the handshake so avoid creating a new one
+	 * and use the already existing default one instead.
+	 */
+	context = new_hosts->default_host->ssl_ctx;
+
+	/*
+	 * Since we don't allocate a new SSL_CTX here like we do when SNI has been
+	 * enabled we need to bump the reference count on context to avoid double
+	 * free of the context when using the same cleanup logic across the cases.
+	 */
+	SSL_CTX_up_ref(context);
+#endif
+
+	/*
+	 * Disable OpenSSL's moving-write-buffer sanity check, because it causes
+	 * unnecessary failures in nonblocking send cases.
+	 */
+	SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
 
 	if (ssl_min_protocol_version)
 	{
@@ -323,20 +503,186 @@ be_tls_init(bool isServerStart)
 	if (SSLPreferServerCiphers)
 		SSL_CTX_set_options(context, SSL_OP_CIPHER_SERVER_PREFERENCE);
 
+	/*
+	 * Success!  Replace any existing SSL_context and host configurations.
+	 */
+	if (SSL_context)
+	{
+		SSL_CTX_free(SSL_context);
+		SSL_context = NULL;
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+
+	if (SSL_hosts_memcxt)
+		MemoryContextDelete(SSL_hosts_memcxt);
+
+	SSL_hosts_memcxt = host_memcxt;
+	SSL_hosts = new_hosts;
+	SSL_context = context;
+
+	return 0;
+
+	/*
+	 * Clean up by releasing working SSL contexts as well as allocations
+	 * performed during parsing.  Since all our allocations are done in a
+	 * local memory context all we need to do is delete it.
+	 */
+error:
+	if (context)
+		SSL_CTX_free(context);
+
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(host_memcxt);
+	return -1;
+}
+
+/*
+ * host_context_cleanup_cb
+ *
+ * Memory context reset callback for clearing OpenSSL managed resources when
+ * hosts are reloaded and the previous set of configured hosts are freed. As
+ * all hosts are allocated in a single context we don't need to free each host
+ * individually, just resources managed by OpenSSL.
+ */
+static void
+host_context_cleanup_cb(void *arg)
+{
+	struct hosts *hosts = arg;
+
+	foreach_ptr(HostsLine, host, hosts->sni)
+	{
+		if (host->ssl_ctx != NULL)
+			SSL_CTX_free(host->ssl_ctx);
+	}
+
+	if (hosts->no_sni && hosts->no_sni->ssl_ctx)
+		SSL_CTX_free(hosts->no_sni->ssl_ctx);
+
+	if (hosts->default_host && hosts->default_host->ssl_ctx)
+		SSL_CTX_free(hosts->default_host->ssl_ctx);
+}
+
+static bool
+init_host_context(HostsLine *host, bool isServerStart)
+{
+	SSL_CTX    *ctx = SSL_CTX_new(SSLv23_method());
+
+	if (!ctx)
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errmsg("could not create SSL context: %s",
+						SSLerrmessage(ERR_get_error()))));
+		goto error;
+	}
+
+	/*
+	 * Call init hook (usually to set password callback) in case SNI hasn't
+	 * been enabled. If SNI is enabled the hook won't operate on the actual
+	 * TLS context used so it cannot function properly. TODO: issue a warning
+	 * in case there is a non-default hook installed and SNI is enabled.
+	 *
+	 * If SNI is enabled, we set password callback based what was configured.
+	 */
+	if (!ssl_sni)
+		(*openssl_tls_init_hook) (ctx, isServerStart);
+	else
+	{
+		/*
+		 * Set up the password callback, if configured.
+		 */
+		if (isServerStart)
+		{
+			if (host->ssl_passphrase_cmd && host->ssl_passphrase_cmd[0])
+			{
+				SSL_CTX_set_default_passwd_cb(ctx, ssl_external_passwd_cb);
+				SSL_CTX_set_default_passwd_cb_userdata(ctx, host->ssl_passphrase_cmd);
+			}
+		}
+		else
+		{
+			if (host->ssl_passphrase_reload && host->ssl_passphrase_cmd[0])
+			{
+				SSL_CTX_set_default_passwd_cb(ctx, ssl_external_passwd_cb);
+				SSL_CTX_set_default_passwd_cb_userdata(ctx, host->ssl_passphrase_cmd);
+			}
+			else
+			{
+				/*
+				 * If reloading and no external command is configured,
+				 * override OpenSSL's default handling of passphrase-protected
+				 * files, because we don't want to prompt for a passphrase in
+				 * an already-running server.
+				 */
+				SSL_CTX_set_default_passwd_cb(ctx, dummy_ssl_passwd_cb);
+			}
+		}
+	}
+
+	/*
+	 * Load and verify server's certificate and private key
+	 */
+	if (SSL_CTX_use_certificate_chain_file(ctx, host->ssl_cert) != 1)
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("could not load server certificate file \"%s\": %s",
+						host->ssl_cert, SSLerrmessage(ERR_get_error()))));
+		goto error;
+	}
+
+	if (!check_ssl_key_file_permissions(host->ssl_key, isServerStart))
+		goto error;
+
+
+	/* used by the callback */
+	ssl_is_server_start = isServerStart;
+
+	/*
+	 * OK, try to load the private key file.
+	 */
+	dummy_ssl_passwd_cb_called = false;
+
+	if (SSL_CTX_use_PrivateKey_file(ctx,
+									host->ssl_key,
+									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",
+							host->ssl_key)));
+		else
+			ereport(isServerStart ? FATAL : LOG,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not load private key file \"%s\": %s",
+							host->ssl_key, SSLerrmessage(ERR_get_error()))));
+		goto error;
+	}
+
+	if (SSL_CTX_check_private_key(ctx) != 1)
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("check of private key failed: %s",
+						SSLerrmessage(ERR_get_error()))));
+		goto error;
+	}
+
 	/*
 	 * Load CA store, so we can verify client certificates if needed.
 	 */
-	if (ssl_ca_file[0])
+	if (host->ssl_ca && host->ssl_ca[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(ctx, host->ssl_ca, NULL) != 1 ||
+			(root_cert_list = SSL_load_client_CA_file(host->ssl_ca)) == 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()))));
+							host->ssl_ca, SSLerrmessage(ERR_get_error()))));
 			goto error;
 		}
 
@@ -347,17 +693,7 @@ be_tls_init(bool isServerStart)
 		 * that the SSL context will "own" the root_cert_list and remember to
 		 * 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);
+		SSL_CTX_set_client_CA_list(ctx, root_cert_list);
 	}
 
 	/*----------
@@ -367,7 +703,7 @@ be_tls_init(bool isServerStart)
 	 */
 	if (ssl_crl_file[0] || ssl_crl_dir[0])
 	{
-		X509_STORE *cvstore = SSL_CTX_get_cert_store(context);
+		X509_STORE *cvstore = SSL_CTX_get_cert_store(ctx);
 
 		if (cvstore)
 		{
@@ -408,29 +744,13 @@ 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;
+	host->ssl_ctx = ctx;
+	return true;
 
-	/* Clean up by releasing working context. */
 error:
-	if (context)
-		SSL_CTX_free(context);
-	return -1;
+	if (ctx)
+		SSL_CTX_free(ctx);
+	return false;
 }
 
 void
@@ -486,6 +806,38 @@ be_tls_open_server(Port *port)
 		return -1;
 	}
 
+	/*
+	 * If the underlying TLS library supports the client hello callback we use
+	 * that in order to support host based configuration using the SNI TLS
+	 * extension.  If the user has disabled SNI via the ssl_sni GUC we still
+	 * make use of the callback in order to have consistent handling of
+	 * OpenSSL contexts, except in that case the callback will install the
+	 * default configuration regardless of the hostname sent by the user in
+	 * the handshake.
+	 *
+	 * In case the TLS library does not support the client hello callback, as
+	 * of this writing LibreSSL does not, we need to install the client cert
+	 * verification callback here (if the user configured a CA) since we
+	 * cannot use the OpenSSL context update functionality.
+	 */
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+	SSL_CTX_set_client_hello_cb(SSL_context, sni_clienthello_cb, NULL);
+#else
+	if (SSL_hosts->default_host->ssl_ca && SSL_hosts->default_host->ssl_ca[0])
+	{
+		/*
+		 * 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_set_verify(port->ssl,
+					   (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+					   verify_cb);
+
+		ssl_loaded_verify_locations = true;
+	}
+#endif
+
 	err_context.cert_errdetail = NULL;
 	SSL_set_ex_data(port->ssl, 0, &err_context);
 
@@ -1142,10 +1494,11 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
 {
 	/* same prompt as OpenSSL uses internally */
 	const char *prompt = "Enter PEM pass phrase:";
+	const char *cmd = userdata;
 
 	Assert(rwflag == 0);
 
-	return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+	return run_ssl_passphrase_command(cmd, prompt, ssl_is_server_start, buf, size);
 }
 
 /*
@@ -1391,6 +1744,258 @@ alpn_cb(SSL *ssl,
 	}
 }
 
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+/*
+ * ssl_update_ssl
+ *
+ * Replace certificate/key and CA in an SSL object to match the, via the SNI
+ * extension, selected host configuration for the connection.  The SSL_CTX
+ * object to use should be passed in as ctx.  This function will update the
+ * SSL object in-place.
+ */
+static bool
+ssl_update_ssl(SSL *ssl, HostsLine *host_config)
+{
+	SSL_CTX    *ctx = host_config->ssl_ctx;
+
+	X509	   *cert;
+	EVP_PKEY   *key;
+
+	STACK_OF(X509) * chain;
+
+	Assert(ctx != NULL);
+	/*-
+	 * Make use of the already-loaded certificate chain and key. At first
+	 * glance, SSL_set_SSL_CTX() looks like the easiest way to do this, but
+	 * beware -- it has very odd behavior:
+	 *
+	 *     https://github.com/openssl/openssl/issues/6109
+	 */
+	cert = SSL_CTX_get0_certificate(ctx);
+	key = SSL_CTX_get0_privatekey(ctx);
+
+	Assert(cert && key);
+
+	if (!SSL_CTX_get0_chain_certs(ctx, &chain)
+		|| !SSL_use_cert_and_key(ssl, cert, key, chain, 1 /* override */ )
+		|| !SSL_check_private_key(ssl))
+	{
+		/*
+		 * This shouldn't really be possible, since the inputs came from a
+		 * SSL_CTX that was already populated by OpenSSL.
+		 */
+		ereport(COMMERROR,
+				errcode(ERRCODE_INTERNAL_ERROR),
+				errmsg_internal("could not update certificate chain: %s",
+								SSLerrmessage(ERR_get_error())));
+		return false;
+	}
+
+	if (host_config->ssl_ca && host_config->ssl_ca[0])
+	{
+		/*
+		 * Copy the trust store and list of roots over from the SSL_CTX.
+		 */
+		X509_STORE *ca_store = SSL_CTX_get_cert_store(ctx);
+
+		STACK_OF(X509_NAME) * roots;
+
+		/*
+		 * The trust store appears to be the only setting that this function
+		 * can't override via the (SSL *) pointer directly. Instead, share it
+		 * with the active SSL_CTX (this should always be SSL_context).
+		 */
+		Assert(SSL_context == SSL_get_SSL_CTX(ssl));
+		SSL_CTX_set1_cert_store(SSL_context, ca_store);
+
+		/*
+		 * TODO: test that the new locations don't stack with prior CA config;
+		 * that's CVE-worthy
+		 *
+		 * TODO: test interactions with CRLs.
+		 */
+
+		/*
+		 * SSL_set_client_CA_list() will take ownership of its argument, so we
+		 * need to duplicate it.
+		 */
+		if ((roots = SSL_CTX_get_client_CA_list(ctx)) == NULL
+			|| (roots = SSL_dup_CA_list(roots)) == NULL)
+		{
+			ereport(COMMERROR,
+					errcode(ERRCODE_INTERNAL_ERROR),
+					errmsg_internal("could not duplicate SSL_CTX CA list: %s",
+									SSLerrmessage(ERR_get_error())));
+			return false;
+		}
+
+		SSL_set_client_CA_list(ssl, roots);
+
+		/*
+		 * 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_set_verify(ssl,
+					   (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+					   verify_cb);
+
+		ssl_loaded_verify_locations = true;
+	}
+
+	return true;
+}
+
+/*
+ * sni_clienthello_cb
+ *
+ * Callback for extracting the servername extension from the TLS handshake
+ * during ClientHello.  There is a callback in OpenSSL for the servername
+ * specifically but OpenSSL themselves advice against using it as it is more
+ * dependent on ordering for execution.
+ */
+static int
+sni_clienthello_cb(SSL *ssl, int *al, void *arg)
+{
+	const char *tlsext_hostname;
+	const unsigned char *tlsext;
+	size_t		left,
+				len;
+	HostsLine  *install_config = NULL;
+
+	if (!ssl_sni)
+	{
+		install_config = SSL_hosts->default_host;
+		goto found;
+	}
+
+	if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &tlsext, &left))
+	{
+		if (left <= 2)
+		{
+			*al = SSL_AD_MISSING_EXTENSION;
+			return 0;
+		}
+		len = (*(tlsext++) << 8);
+		len += *(tlsext)++;
+		if (len + 2 != left)
+		{
+			*al = SSL_AD_MISSING_EXTENSION;
+			return 0;
+		}
+
+		left = len;
+
+		if (left == 0 || *tlsext++ != TLSEXT_NAMETYPE_host_name)
+		{
+			*al = SSL_AD_MISSING_EXTENSION;
+			return 0;
+		}
+
+		left--;
+
+		/*
+		 * Now we can finally pull out the byte array with the actual
+		 * hostname.
+		 */
+		if (left <= 2)
+		{
+			*al = SSL_AD_MISSING_EXTENSION;
+			return 0;
+		}
+		len = (*(tlsext++) << 8);
+		len += *(tlsext++);
+		if (len + 2 > left)
+		{
+			*al = SSL_AD_MISSING_EXTENSION;
+			return 0;
+		}
+		left = len;
+		tlsext_hostname = (const char *) tlsext;
+
+		/*
+		 * We have a requested hostname from the client, match against all
+		 * entries in the pg_hosts configuration and attempt to find a match.
+		 * Matching is done case insensitive as per RFC 952 and RFC 921.
+		 */
+		foreach_ptr(HostsLine, host, SSL_hosts->sni)
+		{
+			foreach_ptr(char, hostname, host->hostnames)
+			{
+				if (strlen(hostname) == len &&
+					pg_strncasecmp(hostname, tlsext_hostname, len) == 0)
+				{
+					install_config = host;
+					goto found;
+				}
+			}
+		}
+
+		/*
+		 * If no host specific match was found, and there is a default config,
+		 * then fall back to using that.
+		 */
+		if (!install_config && SSL_hosts->default_host)
+			install_config = SSL_hosts->default_host;
+	}
+
+	/*
+	 * No hostname TLS extension in the handshake, use the default or no_sni
+	 * configurations if available.
+	 */
+	else
+	{
+		if (SSL_hosts->no_sni)
+			install_config = SSL_hosts->no_sni;
+		else if (SSL_hosts->default_host)
+			install_config = SSL_hosts->default_host;
+		else
+		{
+			/*
+			 * Reaching here means that we didn't get a hostname in the TLS
+			 * extension and the server has been configured to not allow any
+			 * connections without a specified hostname.
+			 *
+			 * 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, and no fallback configured")));
+			return SSL_CLIENT_HELLO_ERROR;
+		}
+	}
+
+	/*
+	 * If we reach here without a context chosen as the session context then
+	 * fail the handshake and terminate the connection.
+	 */
+	if (install_config == NULL)
+	{
+		if (tlsext_hostname)
+			*al = SSL_AD_UNRECOGNIZED_NAME;
+		else
+			*al = SSL_AD_MISSING_EXTENSION;
+		return SSL_CLIENT_HELLO_ERROR;
+	}
+
+found:
+	if (!ssl_update_ssl(ssl, install_config))
+	{
+		ereport(COMMERROR,
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("failed to switch to SSL configuration for host, terminating connection"));
+		return SSL_CLIENT_HELLO_ERROR;
+	}
+
+	return SSL_CLIENT_HELLO_SUCCESS;
+}
+#endif							/* HAVE_SSL_CTX_SET_CLIENT_HELLO_CB */
 
 /*
  * Set DH parameters for generating ephemeral DH keys.  The
@@ -1798,12 +2403,18 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
 	if (isServerStart)
 	{
 		if (ssl_passphrase_command[0])
+		{
 			SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+			SSL_CTX_set_default_passwd_cb_userdata(context, ssl_passphrase_command);
+		}
 	}
 	else
 	{
 		if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+		{
 			SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+			SSL_CTX_set_default_passwd_cb_userdata(context, ssl_passphrase_command);
+		}
 		else
 
 			/*
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index edd69823b92..617704bb993 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -61,6 +61,9 @@ bool		SSLPreferServerCiphers;
 int			ssl_min_protocol_version = PG_TLS1_2_VERSION;
 int			ssl_max_protocol_version = PG_TLS_ANY;
 
+/* GUC variable: if false, discards hostname extensions in handshake */
+bool		ssl_sni = false;
+
 /* ------------------------------------------------------------ */
 /*			 Procedures common to all secure sessions			*/
 /* ------------------------------------------------------------ */
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index ee337cf42cc..8571f652844 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 d77502838c4..e1546d9c97a 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,37 @@ 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);
+		goto fail;
+	}
+	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 5ee84a639d8..f009a5caa3e 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1177,6 +1177,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',
@@ -2764,6 +2771,14 @@
   max => '0',
 },
 
+{ name => 'ssl_sni', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+  short_desc => 'Sets whether to interpret SNI extensions in SSL connections.',
+  flags => 'GUC_SUPERUSER_ONLY',
+  variable => 'ssl_sni',
+  boot_val => 'false',
+  check_nook => 'check_ssl_sni',
+},
+
 { 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 38aaf82f120..1e14b7b4af0 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -565,6 +565,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 e686d88afc4..e4abe6c0077 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
@@ -122,6 +124,7 @@
 #ssl_dh_params_file = ''
 #ssl_passphrase_command = ''
 #ssl_passphrase_command_supports_reload = off
+#ssl_sni = off
 
 
 #------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index f3174d79f32..509f1114ef6 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;
@@ -1547,6 +1548,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);
@@ -2808,6 +2817,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");
@@ -2823,12 +2833,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);
 	}
@@ -2836,6 +2846,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..bbc6a97ccdc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,36 @@ typedef struct IdentLine
 	AuthToken  *pg_user;
 } IdentLine;
 
+typedef struct HostsLine
+{
+	int			linenumber;
+
+	char	   *sourcefile;
+	char	   *rawline;
+
+	/* Required fields */
+	List	   *hostnames;
+	char	   *ssl_key;
+	char	   *ssl_cert;
+
+	/* Optional fields */
+	char	   *ssl_ca;
+	char	   *ssl_passphrase_cmd;
+	bool		ssl_passphrase_reload;
+
+	/* Internal bookkeeping */
+	void	   *ssl_ctx;		/* associated SSL_CTX* for the above settings */
+} HostsLine;
+
+typedef enum HostsFileLoad
+{
+	HOSTSFILE_LOAD_OK = 0,
+	HOSTSFILE_LOAD_FAILED,
+	HOSTSFILE_EMPTY,
+	HOSTSFILE_MISSING,
+	HOSTSFILE_DISABLED,
+} 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.h b/src/include/libpq/libpq.h
index 790724b6a0b..c9b934d2321 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -113,6 +113,7 @@ extern PGDLLIMPORT int ssl_max_protocol_version;
 extern PGDLLIMPORT char *ssl_passphrase_command;
 extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
 extern PGDLLIMPORT char *ssl_dh_params_file;
+extern PGDLLIMPORT bool ssl_sni;
 extern PGDLLIMPORT char *SSLCipherSuites;
 extern PGDLLIMPORT char *SSLCipherList;
 extern PGDLLIMPORT char *SSLECDHCurve;
@@ -158,9 +159,11 @@ 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,
+extern int	run_ssl_passphrase_command(const char *cmd, 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 int	load_hosts(List **hosts, char **err_msg);
 
 #endif							/* LIBPQ_H */
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cb0f53fade4..bb9ea39bd60 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -368,6 +368,9 @@
 /* Define to 1 if you have the `SSL_CTX_set_ciphersuites' function. */
 #undef HAVE_SSL_CTX_SET_CIPHERSUITES
 
+/* Define to 1 if you have the `SSL_CTX_set_client_hello_cb' function. */
+#undef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+
 /* Define to 1 if you have the `SSL_CTX_set_keylog_callback' function. */
 #undef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index c46203fabfe..dc406d6651a 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/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 9c90670d9b8..b01697c1f60 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -133,6 +133,7 @@ extern void assign_session_authorization(const char *newval, void *extra);
 extern void assign_session_replication_role(int newval, void *extra);
 extern void assign_stats_fetch_consistency(int newval, void *extra);
 extern bool check_ssl(bool *newval, void **extra, GucSource source);
+extern bool check_ssl_sni(bool *newval, void **extra, GucSource source);
 extern bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
 extern bool check_standard_conforming_strings(bool *newval, void **extra,
 											  GucSource source);
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..b44aefb545a 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 9e5bdbb6136..d7e7ce23433 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 a86e8ff0e86..3574c10599a 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.
@@ -1014,22 +1014,31 @@ $node->connect_fails(
 	"$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt"
 	  . sslkey('client.key'),
 	"host: 'example.org', ca: '': connect with sslcert, no client CA configured",
-	expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
+	expected_stderr =>
+	  qr/client certificates can only be checked if a root certificate store is available/
+);
 
 # example.com uses the client CA.
-switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root+client_ca');
+switch_server_cert(
+	$node,
+	certfile => 'server-cn-only',
+	cafile => 'root+client_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: 'root+client_ca.crt': 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'),
+	"$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
 	"host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent"
 );
 
 # example.net uses the server CA (which is wrong).
-switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root+server_ca');
+switch_server_cert(
+	$node,
+	certfile => 'server-cn-only',
+	cafile => 'root+server_ca');
 # example.net is configured and should require a client cert, but will
 # always fail verification.
 $node->connect_fails(
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..eab4b4f1310
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,396 @@
+
+# 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();
+
+if ($ssl_server->is_libressl)
+{
+	plan skip_all => 'SNI not supported when building with LibreSSL';
+}
+
+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/);
+
+# Add an entry in pg_hosts.conf with no default, and reload. Since ssl_sni is
+# still 'off' we should still be able to connect using the certificates in
+# postgresql.conf
+$node->append_conf('pg_hosts.conf',
+	"example.org server-cn-only.crt server-cn-only.key");
+$node->reload;
+$node->connect_ok(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"pg.conf: connect with correct server CA cert file sslmode=require");
+
+# Turn on SNI support and remove pg_hosts.conf and reload to make sure a
+# missing file is treated like an empty file.
+$node->append_conf('postgresql.conf', 'ssl_sni = on');
+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_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");
+
+# Use multiple hostnames for a single configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	"example.org,example.com,example.net 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_ok(
+	"$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	"pg_hosts.conf: connect to example.com and verify server CA");
+$node->connect_ok(
+	"$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	"pg_hosts.conf: connect to example.net and verify server CA");
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.se",
+	"pg_hosts.conf: connect to default with sslmode=require",
+	expected_stderr => qr/unrecognized name/);
+
+# Test @-inclusion of hostnames.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	'example.org,@hostnames.txt server-cn-only+server_ca.crt server-cn-only.key root_ca.crt'
+);
+$node->append_conf(
+	'hostnames.txt', qq{
+example.com
+example.net
+});
+$node->reload;
+
+$node->connect_ok(
+	"$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	'@hostnames.txt: connect to example.org and verify server CA');
+$node->connect_ok(
+	"$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	'@hostnames.txt: connect to example.com and verify server CA');
+$node->connect_ok(
+	"$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+	'@hostnames.txt: connect to example.net and verify server CA');
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.se",
+	'@hostnames.txt: connect to default with sslmode=require',
+	expected_stderr => qr/unrecognized name/);
+
+# Add an incorrect entry specifying a default entry combined with hostnames
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	"example.org,*,example.net server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+	'pg_hosts.conf: restart fails with default entry combined with hostnames'
+);
+
+# Add incorrect duplicate entries.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+	'pg_hosts.conf', qq{
+* server-cn-only.crt server-cn-only.key
+* server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two default entries');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+	'pg_hosts.conf', qq{
+/no_sni/ server-cn-only.crt server-cn-only.key
+/no_sni/ server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two no_sni entries');
+
+# TODO: duplicate SNI hostnames?
+
+# 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->restart;
+
+# 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/handshake failure/);
+$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/);
+$node->connect_fails(
+	"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example",
+	"pg_hosts.conf: connect to 'example' 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'
+);
+$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. Restarting should not give any errors in the log, but the
+# subsequent reload should fail with an error regarding reloading.
+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'
+);
+my $node_loglocation = -s $node->logfile;
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+	'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+my $log =
+  PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+	$log,
+	qr/cannot be reloaded because it requires a passphrase/,
+	'log reload failure due to passphrase command reloading');
+
+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"
+	);
+	# Reloading should fail since the passphrase cannot be reloaded, with an
+	# error recorded in the log.  Since we keep existing contexts around it
+	# should still work.
+	$node_loglocation = -s $node->logfile;
+	$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 sslmode=require"
+	);
+	$log =
+	  PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+	like(
+		$log,
+		qr/cannot be reloaded because it requires a passphrase/,
+		'log reload failure due to passphrase command reloading');
+}
+
+# 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->restart;
+
+$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
+
+# 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->restart;
+
+$connstr =
+  "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+# 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: '': 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",
+	"host: 'example.com', ca: 'root+client_ca.crt': 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 "
+	  . $ssl_server->sslkey('client.key'),
+	"host: 'example.com', ca: 'root+client_ca.crt': 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: 'root+server_ca.crt': 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: 'root+server_ca.crt': connect with sslcert, client certificate sent",
+	expected_stderr => qr/unknown ca/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3250564d4ff..4591d47490c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1227,6 +1227,8 @@ HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
 HistControl
+HostsFileLoadResult
+HostsLine
 HotStandbyState
 I32
 ICU_Convert_Func
-- 
2.39.3 (Apple Git-146)

