diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index c7acc0f182f3..b4f48da02a36 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
+
+
+ ssl_snimode (enum)
+
+ ssl_snimode configuration parameter
+
+
+
+
+ This parameter determines if the server will inspect the SNI TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: off, default and strict.
+
+
+
+
+ off
+
+
+ SNI is not enabled and no configuration from
+ pg_hosts.conf is loaded. Configuration of SSL
+ for all connections is done with ,
+ and .
+
+
+
+
+
+ default
+
+
+ SNI is enabled and hostname configuration is loaded from
+ pg_hosts.conf. ,
+ and
+ are loaded as the default configuration. Connections specifying
+ to 1
+ will be attempted using the default configuration if the hostname
+ is missing in pg_hosts.conf. If the hostname
+ matches an entry from pg_hosts.conf, then the
+ configuration from that entry will be used for setting up the
+ connection.
+
+
+
+
+
+ strict
+
+
+ SNI is enabled and all connections are required to set to 1 and
+ specify a hostname matching an entry in
+ pg_hosts.conf. Any connection without or with a hostname missing from
+ pg_hosts.conf will be rejected.
+ ,
+ and
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+
+
+
+
+
+
+
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac635..fa6fe07adc08 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
client certificate must not be on this list
+
+ $PGDATA/pg_hosts.conf
+ SNI configuration
+ defines which certificates to use for which server hostname
+
+
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
+
+ SNI Configuration
+
+
+ PostgreSQL can be configured for
+ SNI using the pg_hosts.conf
+ configuration file. PostgreSQL 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.
+
+
+
+ SNI configuration is defined in the hosts configuration file,
+ pg_hosts.conf, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+
+hostname SSL_certificate SSL_key SSL_CA_certificate SSL_passphrase_cmd SSL_passphrase_cmd_reload
+include file
+include_if_exists file
+include_dir directory
+
+ Comments, whitespace and line continuations are handled in the same way as
+ in pg_hba.conf. hostname
+ is matched against the hostname TLS extension in the SSL handshake.
+ SSL_certificate,
+ SSL_key,
+ SSL_CA_certificate,
+ SSL_passphrase_cmd, and
+ SSL_passphrase_cmd_reload
+ are treated like
+ ,
+ ,
+ ,
+ , and
+ respectively.
+ All fields except SSL_passphrase_cmd and
+ SSL_passphrase_cmd_reload are required. If
+ SSL_passphrase_cmd is defined but not
+ SSL_passphrase_cmd_reload then the default
+ value for SSL_passphrase_cmd_reload is
+ off.
+
+
+ The SSL configuration from postgresql.conf is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When is set to
+ default this configuration will be the defualt fallback
+ if no matching hostname is found in pg_hosts.conf. If
+ is set to strict it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+
+
+ It is currently not possible to set different clientname
+ values for the different certificates. Any clientname
+ setting in pg_hba.conf will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+
+
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c6..2d1691c7950a 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa78..67a50c7b24c3 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +181,196 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ hostcxt = AllocSetContextCreate(PostmasterContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("no SNI configuration added from configuration file \"%s\"",
+ HostsFileName));
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ if (!ok)
+ {
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index c8b63ef82490..14ff1d78c40c 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+ bool ssl_passphrase_support_reload;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +90,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +111,160 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+
+ /*
+ * If there are contexts loaded when we init they must be released.
+ */
+ if (contexts != NIL)
+ {
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate")));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list.
+ */
+ sni_hosts = load_hosts();
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that. The passphrase command fields are
+ * however optional so we need to check whether those were set.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+ if (host->ssl_passphrase_cmd != NULL)
+ host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+ host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded")));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +290,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +308,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +326,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +490,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +572,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -759,6 +921,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1132,7 +1297,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1534,88 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse.
+ */
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * In ssl_snimode "strict" it's an error if there was no match for the
+ * hostname in the TLS extension. Terminate the connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1825,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +2024,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2052,26 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ if (host->ssl_passphrase)
+ pfree(unconstify(char *, host->ssl_passphrase));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e8137..1431f92e3321 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1ec..4f6ec13bc741 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 000000000000..5a47f9cae7dc
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 667df448732f..c43a259d82e9 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ /*
+ * Likewise for pg_hosts.conf
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index d14b1678e7fe..806706f1bedb 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -555,6 +562,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4711,6 +4719,17 @@ struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+ gettext_noop("Sets the server's \"hosts\" configuration file."),
+ NULL,
+ GUC_SUPERUSER_ONLY
+ },
+ &HostsFileName,
+ NULL,
+ NULL, NULL, NULL
+ },
+
{
{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5386,6 +5405,18 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Sets the SNI mode to use."),
+ NULL,
+ GUC_SUPERUSER_ONLY,
+ },
+ &ssl_snimode,
+ SSL_SNIMODE_DEFAULT,
+ ssl_snimode_options,
+ NULL, NULL, NULL
+ },
+
{
{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a9d8293474af..57b1be3c38fa 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 62bbd08d9f65..a4e2b36759bc 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);
@@ -2794,6 +2803,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");
@@ -2809,12 +2819,13 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n"
+ "PG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2822,6 +2833,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 3657f182db3e..3d8e33533b85 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a63825..e1631cb7b5c5 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 aeb66ca40cf3..5feed0eb0a46 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -134,12 +135,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f619100467df..025e7e95e906 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821d..a85d85735cf2 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0a..e5a9402cd9cc 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 000000000000..f0ce048273ac
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,164 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=struct",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ff050e93a507..0666ff16d33c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1202,6 +1202,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func