public inbox for [email protected]
help / color / mirror / Atom feedRe: Serverside SNI support in libpq
58+ messages / 10 participants
[nested] [flat]
* Re: Serverside SNI support in libpq
@ 2024-12-03 13:58 Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2024-12-03 13:58 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Pgsql Hackers <[email protected]>
> On 25 Jul 2024, at 19:51, Jacob Champion <[email protected]> wrote:
The attached rebased version adds proper list reset, a couple of bugfixes
around cert loading and the ability to set ssl_passhprase_command (and reload)
in the hosts file.
> Matt Caswell appears to be convinced that SSL_set_SSL_CTX() is
> fundamentally broken. So it might just be FUD, but I'm wondering if we
> should instead be using the SSL_ flavors of the API to reassign the
> certificate chain on the SSL pointer directly, inside the callback,
> instead of trying to set them indirectly via the SSL_CTX_ API.
Maybe, but I would feel better about changing if I can could reproduce the
issues (see below).
> Have you seen any weird behavior like this on your end? I'm starting
> to doubt my test setup...
I've not been able to reproduce any behaviour like what you describe.
> On the plus side, I now have a handful of
> debugging patches for a future commitfest.
Do you have them handy for running tests on this version?
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v2-0001-Serverside-SNI-support-for-libpq.patch (43.2K, 2-v2-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From c8aea86957ad12b7e48a32370eb2c565c20a2205 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v2] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 62 ++++
doc/src/sgml/runtime.sgml | 50 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 201 ++++++++++-
src/backend/libpq/be-secure-openssl.c | 315 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 5 +
src/backend/utils/misc/guc.c | 26 ++
src/backend/utils/misc/guc_tables.c | 31 ++
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/bin/initdb/initdb.c | 16 +-
src/include/libpq/hba.h | 19 ++
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 92 +++++
src/tools/pgindent/typedefs.list | 2 +
19 files changed, 798 insertions(+), 48 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e0c8325a39..1cecaccee7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Any connection specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ a hostname which is missing in <filename>pg_hosts.conf</filename>
+ will be attempted using the default configuration. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 94135e9d5e..0ac79ae28d 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2571,6 +2577,50 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ SSL certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file, which is
+ named <filename>pg_hosts.conf</filename> and is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 84302cc6da..bc8accbde0 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index 0cb201acb1..7c900edd45 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +181,194 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here. */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ hostcxt = AllocSetContextCreate(PostmasterContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("no SNI configuration added from configuration file \"%s\"",
+ HostsFileName));
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ if (!ok)
+ {
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 91a86d62a3..5acfdeb625 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+ bool ssl_passphrase_support_reload;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +90,16 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +110,155 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+
+ /*
+ * If there are contexts loaded when we init they should be released. This
+ * should only be possible when reloading, but to keep any subtle bugs at
+ * arms length we check unconditionally with an assert for non-production
+ * builds.
+ */
+ if (contexts != NIL)
+ {
+ Assert(isServerStart == false);
+ free_contexts();
+ }
+
+ /*
+ * When ssl_snimode is off or default we load the certificate and key
+ * specified in postgresql.conf and set that as the default host.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ HostContext *host_context;
+ HostsLine line;
+
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate")));
+ return -1;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+
+ host_context->hostname = pstrdup("*");
+ host_context->context = ctx;
+ host_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into
+ * SSL_context.
+ */
+ if (ssl_ca_file[0])
+ host_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * The contexts list is not used in ssl_snimode off but we add the
+ * entry there anyways for consistency with the other modes.
+ */
+ contexts = lappend(contexts, host_context);
+
+ /*
+ * Install the default certificate which for ssl_snimode default can
+ * be overridden in the callback if a hostname match is found.
+ */
+ SSL_context = ctx;
+ Host_context = host_context;
+ }
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list.
+ */
+ sni_hosts = load_hosts();
+
+ /*
+ * In strict ssl_snimode there needs to be a working pg_hosts file,
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load pg_hosts.conf file"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+
+ SSL_context = ssl_init_context(isServerStart, host);
+ if (SSL_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ host_context = palloc(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = SSL_context;
+ host_context->default_host = false;
+ if (host->ssl_passphrase_cmd != NULL)
+ host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+ host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded")));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +284,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +302,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +320,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +484,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +566,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -1132,7 +1288,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1525,60 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ ListCell *cell;
+ HostContext *host_context;
+
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ return SSL_TLSEXT_ERR_OK;
+ }
+
+ foreach(cell, contexts)
+ {
+ host_context = lfirst(cell);
+
+ if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host_context;
+ SSL_context = host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we can return without doing anything since we
+ * already installed the context for the default host when parsing the
+ * hosts file.
+ */
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1788,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +1987,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2015,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ if (host->ssl_passphrase)
+ pfree(unconstify(char *, host->ssl_passphrase));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 2139f81f24..ad3066b63c 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 7c65314512..1c6269262c 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 0000000000..608210686e
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
+
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c10c0844ab..b3e1cf0b25 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ /*
+ * Likewise for pg_hosts.conf
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8cf1afbad2..dca5a15f5c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -538,6 +545,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4562,6 +4570,17 @@ struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+ gettext_noop("Sets the server's \"hosts\" configuration file."),
+ NULL,
+ GUC_SUPERUSER_ONLY
+ },
+ &HostsFileName,
+ NULL,
+ NULL, NULL, NULL
+ },
+
{
{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5204,6 +5223,18 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Sets the SNI mode to use."),
+ NULL,
+ GUC_SUPERUSER_ONLY,
+ },
+ &ssl_snimode,
+ SSL_SNIMODE_DEFAULT,
+ ssl_snimode_options,
+ NULL, NULL, NULL
+ },
+
{
{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a2ac7575ca..902c4eccef 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -120,6 +120,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 9a91830783..984763da08 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1512,6 +1513,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2771,6 +2780,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2786,12 +2796,13 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n"
+ "PG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2799,6 +2810,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8ea837ae82..d1eb750368 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 9109b2c334..cf0e87a28c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -314,6 +314,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -326,7 +327,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 07e5b12536..98da8b61eb 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -134,12 +135,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 840b0fe57f..4c33ba7ca4 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -284,6 +284,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..55f5887d9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 0000000000..d2efcd850c
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,92 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require",
+ expected_stderr => qr/unexpected eof/);
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" no');
+my $result = $node->restart(fail_ok => 1);
+is($result, 0, 'restart fails with password-protected key when using the wrong passphrase command');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" no');
+$result = $node->restart(fail_ok => 1);
+is($result, 1, 'restart succeeds with password-protected key when using the correct passphrase command');
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2d4c870423..15ccf4cf46 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1158,6 +1158,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2024-12-04 00:43 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Jacob Champion @ 2024-12-04 00:43 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Pgsql Hackers <[email protected]>
On Tue, Dec 3, 2024 at 5:58 AM Daniel Gustafsson <[email protected]> wrote:
> > Have you seen any weird behavior like this on your end? I'm starting
> > to doubt my test setup...
>
> I've not been able to reproduce any behaviour like what you describe.
Hm, v2 is different enough that I'm going to need to check my notes
and try to reproduce again. At first glance, I am still seeing strange
reload behavior (e.g. issuing `pg_ctl reload` a couple of times in a
row leads to the server disappearing without any log messages
indicating why).
> > On the plus side, I now have a handful of
> > debugging patches for a future commitfest.
>
> Do you have them handy for running tests on this version?
I'll work on cleaning them up. I'd meant to contribute them
individually by now, but I got a bit sidetracked...
Thanks!
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2024-12-04 13:44 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2024-12-04 13:44 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Pgsql Hackers <[email protected]>
> On 4 Dec 2024, at 01:43, Jacob Champion <[email protected]> wrote:
>
> On Tue, Dec 3, 2024 at 5:58 AM Daniel Gustafsson <[email protected]> wrote:
>>> Have you seen any weird behavior like this on your end? I'm starting
>>> to doubt my test setup...
>>
>> I've not been able to reproduce any behaviour like what you describe.
>
> Hm, v2 is different enough that I'm going to need to check my notes
> and try to reproduce again. At first glance, I am still seeing strange
> reload behavior (e.g. issuing `pg_ctl reload` a couple of times in a
> row leads to the server disappearing without any log messages
> indicating why).
>
>>> On the plus side, I now have a handful of
>>> debugging patches for a future commitfest.
>>
>> Do you have them handy for running tests on this version?
>
> I'll work on cleaning them up. I'd meant to contribute them
> individually by now, but I got a bit sidetracked...
No worries, I know you have a big path on your plate right now. The attached
v3 fixes a small buglet in the tests and adds silly reload testing to try and
stress out any issues.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v3-0001-Serverside-SNI-support-for-libpq.patch (45.3K, 2-v3-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 2033491062b359cd62df4ec1560947ae21f0868c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v3] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 62 ++++
doc/src/sgml/runtime.sgml | 50 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 203 ++++++++++-
src/backend/libpq/be-secure-openssl.c | 315 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 5 +
src/backend/utils/misc/guc.c | 26 ++
src/backend/utils/misc/guc_tables.c | 31 ++
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/bin/initdb/initdb.c | 16 +-
src/include/libpq/hba.h | 19 ++
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 135 ++++++++
src/tools/pgindent/typedefs.list | 2 +
19 files changed, 843 insertions(+), 48 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e0c8325a39..1cecaccee7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Any connection specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ a hostname which is missing in <filename>pg_hosts.conf</filename>
+ will be attempted using the default configuration. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 94135e9d5e..0ac79ae28d 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2571,6 +2577,50 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ SSL certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file, which is
+ named <filename>pg_hosts.conf</filename> and is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 84302cc6da..bc8accbde0 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index 0cb201acb1..e483ca7944 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +181,196 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here. */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ hostcxt = AllocSetContextCreate(PostmasterContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("no SNI configuration added from configuration file \"%s\"",
+ HostsFileName));
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ if (!ok)
+ {
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 91a86d62a3..5acfdeb625 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+ bool ssl_passphrase_support_reload;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +90,16 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +110,155 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+
+ /*
+ * If there are contexts loaded when we init they should be released. This
+ * should only be possible when reloading, but to keep any subtle bugs at
+ * arms length we check unconditionally with an assert for non-production
+ * builds.
+ */
+ if (contexts != NIL)
+ {
+ Assert(isServerStart == false);
+ free_contexts();
+ }
+
+ /*
+ * When ssl_snimode is off or default we load the certificate and key
+ * specified in postgresql.conf and set that as the default host.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ HostContext *host_context;
+ HostsLine line;
+
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate")));
+ return -1;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+
+ host_context->hostname = pstrdup("*");
+ host_context->context = ctx;
+ host_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into
+ * SSL_context.
+ */
+ if (ssl_ca_file[0])
+ host_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * The contexts list is not used in ssl_snimode off but we add the
+ * entry there anyways for consistency with the other modes.
+ */
+ contexts = lappend(contexts, host_context);
+
+ /*
+ * Install the default certificate which for ssl_snimode default can
+ * be overridden in the callback if a hostname match is found.
+ */
+ SSL_context = ctx;
+ Host_context = host_context;
+ }
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list.
+ */
+ sni_hosts = load_hosts();
+
+ /*
+ * In strict ssl_snimode there needs to be a working pg_hosts file,
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load pg_hosts.conf file"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+
+ SSL_context = ssl_init_context(isServerStart, host);
+ if (SSL_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ host_context = palloc(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = SSL_context;
+ host_context->default_host = false;
+ if (host->ssl_passphrase_cmd != NULL)
+ host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+ host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded")));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +284,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +302,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +320,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +484,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +566,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -1132,7 +1288,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1525,60 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ ListCell *cell;
+ HostContext *host_context;
+
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ return SSL_TLSEXT_ERR_OK;
+ }
+
+ foreach(cell, contexts)
+ {
+ host_context = lfirst(cell);
+
+ if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host_context;
+ SSL_context = host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we can return without doing anything since we
+ * already installed the context for the default host when parsing the
+ * hosts file.
+ */
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1788,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +1987,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2015,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ if (host->ssl_passphrase)
+ pfree(unconstify(char *, host->ssl_passphrase));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 2139f81f24..ad3066b63c 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 7c65314512..1c6269262c 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 0000000000..608210686e
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
+
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c10c0844ab..b3e1cf0b25 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ /*
+ * Likewise for pg_hosts.conf
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8cf1afbad2..dca5a15f5c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -538,6 +545,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4562,6 +4570,17 @@ struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+ gettext_noop("Sets the server's \"hosts\" configuration file."),
+ NULL,
+ GUC_SUPERUSER_ONLY
+ },
+ &HostsFileName,
+ NULL,
+ NULL, NULL, NULL
+ },
+
{
{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5204,6 +5223,18 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Sets the SNI mode to use."),
+ NULL,
+ GUC_SUPERUSER_ONLY,
+ },
+ &ssl_snimode,
+ SSL_SNIMODE_DEFAULT,
+ ssl_snimode_options,
+ NULL, NULL, NULL
+ },
+
{
{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a2ac7575ca..902c4eccef 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -120,6 +120,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 9a91830783..984763da08 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1512,6 +1513,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2771,6 +2780,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2786,12 +2796,13 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n"
+ "PG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2799,6 +2810,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8ea837ae82..d1eb750368 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 9109b2c334..cf0e87a28c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -314,6 +314,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -326,7 +327,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 07e5b12536..98da8b61eb 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -134,12 +135,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 840b0fe57f..4c33ba7ca4 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -284,6 +284,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..55f5887d9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 0000000000..1e7e397080
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,135 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require",
+ expected_stderr => qr/unexpected eof/);
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on');
+my $result = $node->restart(fail_ok => 1);
+is($result, 0, 'restart fails with password-protected key when using the wrong passphrase command');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+$result = $node->restart(fail_ok => 1);
+is($result, 1, 'restart succeeds with password-protected key when using the correct passphrase command');
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Stress testing during patch debugging and review, unlikely to be merged in
+# this state.
+for (my $i = 0; $i < 100; $i++)
+{
+ if (int(rand(10)) < 3)
+ {
+ ok(unlink($node->data_dir . '/pg_hosts.conf'));
+ $node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+ }
+ $node->append_conf('pg_hosts.conf', 'localhost_' . $i . ' server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+ $node->reload;
+ $node->reload;
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require during reload loop");
+}
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require after reload loop");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off');
+$result = $node->restart(fail_ok => 1);
+is($result, 1, 'restart succeeds with password-protected key when using the correct passphrase command');
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded",
+ expected_stderr => qr/unexpected eof/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2d4c870423..15ccf4cf46 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1158,6 +1158,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2024-12-11 00:34 Michael Paquier <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2024-12-11 00:34 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, Dec 04, 2024 at 02:44:18PM +0100, Daniel Gustafsson wrote:
> No worries, I know you have a big path on your plate right now. The attached
> v3 fixes a small buglet in the tests and adds silly reload testing to try and
> stress out any issues.
Looks like this still fails quite heavily in the CI.. You may want to
look at that.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, 2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2024-12-11 08:13 Daniel Gustafsson <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2024-12-11 08:13 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
> On 11 Dec 2024, at 01:34, Michael Paquier <[email protected]> wrote:
>
> On Wed, Dec 04, 2024 at 02:44:18PM +0100, Daniel Gustafsson wrote:
>> No worries, I know you have a big path on your plate right now. The attached
>> v3 fixes a small buglet in the tests and adds silly reload testing to try and
>> stress out any issues.
>
> Looks like this still fails quite heavily in the CI.. You may want to
> look at that.
Interestingly enough the CFBot hasn't picked up that there are new version
posted and the buildfailure is from the initial patch in the thread, which no
longer applies (as the CFBot righly points out). I'll try posting another
version later today to see if that gets it unstuck.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-02-19 23:12 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-02-19 23:12 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
Attached is a rebase which fixes a few smaller things (and a pgperltidy run);
and adds a paragraph to the docs about how HBA clientname settings can't be
made per certificate set in an SNI config. As discussed with Jacob offlist,
there might be a case for supporting that but it will be a niche usecase within
a niche feature, so rather than complicating the code for something which might
never be used, it's likely better to document it and await feedback.
Are there any blockers for getting this in?
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v5-0001-Serverside-SNI-support-for-libpq.patch (45.9K, 2-v5-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From ed0a6b24a686b85077643dc8d3617957782eac11 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v5] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 62 ++++
doc/src/sgml/runtime.sgml | 57 ++++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 203 +++++++++++-
src/backend/libpq/be-secure-openssl.c | 307 +++++++++++++++---
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 5 +
src/backend/utils/misc/guc.c | 26 ++
src/backend/utils/misc/guc_tables.c | 31 ++
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 16 +-
src/include/libpq/hba.h | 19 ++
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 128 ++++++++
src/tools/pgindent/typedefs.list | 2 +
20 files changed, 839 insertions(+), 50 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9eedcf6f0f4..a10a1a24890 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 59f39e89924..57dfe972100 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2571,6 +2577,57 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 42d4a28e5aa..96adabf9581 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..67a50c7b24c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +181,196 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ hostcxt = AllocSetContextCreate(PostmasterContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("no SNI configuration added from configuration file \"%s\"",
+ HostsFileName));
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ if (!ok)
+ {
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 64ff3ce3d6a..bb3e724a190 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+ bool ssl_passphrase_support_reload;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +90,16 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +110,147 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+
+ /*
+ * If there are contexts loaded when we init they should be released.
+ */
+ if (contexts != NIL)
+ free_contexts();
+
+ /*
+ * When ssl_snimode is off or default we load the SSL configuration
+ * specified in postgresql.conf and set that as the default host.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ HostContext *host_context;
+ HostsLine line;
+
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate")));
+ return -1;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+
+ host_context->hostname = pstrdup("*");
+ host_context->context = ctx;
+ host_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into
+ * SSL_context.
+ */
+ if (ssl_ca_file[0])
+ host_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * The contexts list is not used in ssl_snimode off but we add the
+ * entry there anyways for consistency with the other modes.
+ */
+ contexts = lappend(contexts, host_context);
+
+ /*
+ * Install the default certificate which for ssl_snimode default can
+ * be overridden in the callback if a hostname match is found.
+ */
+ SSL_context = ctx;
+ Host_context = host_context;
+ }
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list.
+ */
+ sni_hosts = load_hosts();
+
+ /* In strict ssl_snimode there needs to be a working pg_hosts file */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+
+ SSL_context = ssl_init_context(isServerStart, host);
+ if (SSL_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = SSL_context;
+ host_context->default_host = false;
+ if (host->ssl_passphrase_cmd != NULL)
+ host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+ host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded")));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +276,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +294,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +312,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +476,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +558,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -1132,7 +1280,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1517,60 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ ListCell *cell;
+ HostContext *host_context;
+
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ return SSL_TLSEXT_ERR_OK;
+ }
+
+ foreach(cell, contexts)
+ {
+ host_context = lfirst(cell);
+
+ if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host_context;
+ SSL_context = host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we can return without doing anything since we
+ * already installed the context for the default host when parsing the
+ * hosts file.
+ */
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1780,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +1979,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2007,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ if (host->ssl_passphrase)
+ pfree(unconstify(char *, host->ssl_passphrase));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 91576f94285..b10e8f995ac 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 0f0421037e4..aed5ec16af2 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..608210686e1
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
+
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 12192445218..1a3e5011b35 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ /*
+ * Likewise for pg_hosts.conf
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3cde94a1759..002e12fe1e4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -538,6 +545,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4622,6 +4630,17 @@ struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+ gettext_noop("Sets the server's \"hosts\" configuration file."),
+ NULL,
+ GUC_SUPERUSER_ONLY
+ },
+ &HostsFileName,
+ NULL,
+ NULL, NULL, NULL
+ },
+
{
{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5264,6 +5283,18 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Sets the SNI mode to use."),
+ NULL,
+ GUC_SUPERUSER_ONLY,
+ },
+ &ssl_snimode,
+ SSL_SNIMODE_DEFAULT,
+ ssl_snimode_options,
+ NULL, NULL, NULL
+ },
+
{
{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 415f253096c..dec30a53a35 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -120,6 +122,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 21a0fe3ecd9..a6e680f7b44 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1542,6 +1543,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2805,6 +2814,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2820,12 +2830,13 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n"
+ "PG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2833,6 +2844,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index b20d0051f7d..a1ea3cf3e8c 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7fe92b15477..a5f07aff046 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -323,6 +323,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -335,7 +336,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 8defcb6de19..6d9332cbe22 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -134,12 +135,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 1233e07d7da..37cb3ecb5ae 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index cf8b2b9303a..7a2a5b8ca8c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..0542c59ebcb
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,128 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require",
+ expected_stderr => qr/unexpected eof/);
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded",
+ expected_stderr => qr/unexpected eof/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index fb39c915d76..4410bb24d53 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1165,6 +1165,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-02-24 21:51 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Jacob Champion @ 2025-02-24 21:51 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Michael Paquier <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, Feb 19, 2025 at 3:13 PM Daniel Gustafsson <[email protected]> wrote:
> Are there any blockers for getting this in?
> + SSL_context = ssl_init_context(isServerStart, host);
I'm still not quite following the rationale behind the SSL_context
assignment. To maybe illustrate, attached are some tests that I
expected to pass, but don't.
After adding an additional host and reloading the config, the behavior
of the original fallback host seems to change. Am I misunderstanding
the designed fallback behavior, have I misdesigned my test, or is this
a bug?
Thanks,
--Jacob
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index 0542c59ebcb..e183a953ede 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -57,6 +57,32 @@ $node->connect_ok(
"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
"connect with correct server CA cert file sslmode=require");
+# This is added only for comparison with the same test case below.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+# Why does this test fail?
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
ok(unlink($node->data_dir . '/pg_hosts.conf'));
$node->append_conf('pg_hosts.conf',
"localhost server.crt server.key root.crt");
Attachments:
[text/plain] tests.diff.txt (1.5K, 2-tests.diff.txt)
download | inline diff:
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index 0542c59ebcb..e183a953ede 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -57,6 +57,32 @@ $node->connect_ok(
"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
"connect with correct server CA cert file sslmode=require");
+# This is added only for comparison with the same test case below.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+# Why does this test fail?
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
ok(unlink($node->data_dir . '/pg_hosts.conf'));
$node->append_conf('pg_hosts.conf',
"localhost server.crt server.key root.crt");
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-02-27 13:38 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2025-02-27 13:38 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Michael Paquier <[email protected]>; Pgsql Hackers <[email protected]>
> On 24 Feb 2025, at 22:51, Jacob Champion <[email protected]> wrote:
>
> On Wed, Feb 19, 2025 at 3:13 PM Daniel Gustafsson <[email protected]> wrote:
>> Are there any blockers for getting this in?
>
>> + SSL_context = ssl_init_context(isServerStart, host);
>
> I'm still not quite following the rationale behind the SSL_context
> assignment. To maybe illustrate, attached are some tests that I
> expected to pass, but don't.
>
> After adding an additional host and reloading the config, the behavior
> of the original fallback host seems to change. Am I misunderstanding
> the designed fallback behavior, have I misdesigned my test, or is this
> a bug?
Thanks for the tests, they did in fact uncover a bug in how fallback was
handled which is now fixed. In doing so I revamped how the default context
handling is done, it now always use the GUCs in postgresql.conf for
consistency. The attached v6 rebase contains this as well as your tests as
well as general cleanup and comment writing etc.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v6-0001-Serverside-SNI-support-for-libpq.patch (50.3K, 2-v6-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From ee5508fa4a2b114a6493d60d923b6586250713c5 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 27 Feb 2025 14:03:31 +0100
Subject: [PATCH v6] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 ++++
doc/src/sgml/runtime.sgml | 67 ++++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 203 +++++++++-
src/backend/libpq/be-secure-openssl.c | 356 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 26 ++
src/backend/utils/misc/guc_tables.c | 31 ++
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 16 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 164 ++++++++
src/tools/pgindent/typedefs.list | 2 +
20 files changed, 937 insertions(+), 50 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e55700f35b8..61f3178df82 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1678,6 +1678,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 59f39e89924..1e8f06ba2ce 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2571,6 +2577,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the defualt fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 42d4a28e5aa..96adabf9581 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..67a50c7b24c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +181,196 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ hostcxt = AllocSetContextCreate(PostmasterContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("no SNI configuration added from configuration file \"%s\"",
+ HostsFileName));
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ if (!ok)
+ {
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 64ff3ce3d6a..29544efa667 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+ bool ssl_passphrase_support_reload;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +90,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +111,160 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+
+ /*
+ * If there are contexts loaded when we init they must be released.
+ */
+ if (contexts != NIL)
+ {
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate")));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list.
+ */
+ sni_hosts = load_hosts();
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that. The passphrase command fields are
+ * however optional so we need to check whether those were set.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+ if (host->ssl_passphrase_cmd != NULL)
+ host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+ host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded")));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +290,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +308,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +326,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +490,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +572,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -759,6 +921,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1132,7 +1297,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1534,88 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse.
+ */
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * In ssl_snimode "strict" it's an error if there was no match for the
+ * hostname in the TLS extension. Terminate the connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1825,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +2024,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2052,26 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ if (host->ssl_passphrase)
+ pfree(unconstify(char *, host->ssl_passphrase));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 91576f94285..b10e8f995ac 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 12192445218..1a3e5011b35 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ /*
+ * Likewise for pg_hosts.conf
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39c5..b7e868b128e 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -476,6 +476,13 @@ static const struct config_enum_entry wal_compression_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -540,6 +547,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4624,6 +4632,17 @@ struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+ gettext_noop("Sets the server's \"hosts\" configuration file."),
+ NULL,
+ GUC_SUPERUSER_ONLY
+ },
+ &HostsFileName,
+ NULL,
+ NULL, NULL, NULL
+ },
+
{
{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5277,6 +5296,18 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Sets the SNI mode to use."),
+ NULL,
+ GUC_SUPERUSER_ONLY,
+ },
+ &ssl_snimode,
+ SSL_SNIMODE_DEFAULT,
+ ssl_snimode_options,
+ NULL, NULL, NULL
+ },
+
{
{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5362ff80519..a20a5ba5d75 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -120,6 +122,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
# OAuth
#oauth_validator_libraries = '' # comma-separated list of trusted validator modules
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 21a0fe3ecd9..a6e680f7b44 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1542,6 +1543,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2805,6 +2814,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2820,12 +2830,13 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n"
+ "PG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2833,6 +2844,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 3657f182db3..3d8e33533b8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7fe92b15477..a5f07aff046 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -323,6 +323,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -335,7 +336,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index aeb66ca40cf..5feed0eb0a4 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -134,12 +135,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 1233e07d7da..37cb3ecb5ae 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index cf8b2b9303a..7a2a5b8ca8c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..f0ce048273a
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,164 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=struct",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index cfbab589d61..96c91831feb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1168,6 +1168,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-03-04 21:57 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Jacob Champion @ 2025-03-04 21:57 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Michael Paquier <[email protected]>; Pgsql Hackers <[email protected]>
On Thu, Feb 27, 2025 at 5:38 AM Daniel Gustafsson <[email protected]> wrote:
> Thanks for the tests, they did in fact uncover a bug in how fallback was
> handled which is now fixed. In doing so I revamped how the default context
> handling is done, it now always use the GUCs in postgresql.conf for
> consistency. The attached v6 rebase contains this as well as your tests as
> well as general cleanup and comment writing etc.
Great, thanks!
Revisiting my concerns from upthread:
On Thu, Jul 25, 2024 at 10:51 AM Jacob Champion
<[email protected]> wrote:
> I tried patching all that, but I continue to see nondeterministic
> behavior, including the wrong certificate chain occasionally being
> served, and the servername callback being called twice for each
> connection (?!).
1) The wrong chain being served was due to the fallback bug, now fixed.
2) The servername callback happening twice is due to the TLS 1.3
HelloRetryRequest problem with our ssl_groups (which reminded me to
ping that thread [1]). Switching to TLSv1.2 in order to more easily
see the handshake on the wire makes the problem go away, which
probably did not help my sense of growing insanity last July.
> https://github.com/openssl/openssl/issues/6109
>
> Matt Caswell appears to be convinced that SSL_set_SSL_CTX() is
> fundamentally broken.
We briefly talked about this in Brussels, and I've been trying to find
proof. Attached are some (very rough) tests that might highlight an
issue.
Basically, the new tests set up three hosts in pg_hosts.conf: one with
no client CA, one with a valid client CA, and one with a
malfunctioning CA (root+server_ca, which can't verify our client
certs). Then it switches out the default CA underneath to make sure it
does not affect the visible behavior, since that CA should not
actually be used in the end.
Unfortunately, the failure modes change depending on the default CA.
If it's not a bug in my tests, I think this may be an indication that
SSL_set_SSL_CTX() isn't fully switching out the client verification
behavior? For example, if the default CA isn't set, the other hosts
don't appear to ask for a client certificate even if they need one.
And vice versa.
--
> + /*
> + * Set flag to remember whether CA store has been loaded into this
> + * SSL_context.
> + */
> + if (host->ssl_ca)
I think this should be `if (host->ssl_ca[0])` -- which, incidentally,
fixes one of the new failing tests on my machine.
> int
> be_tls_init(bool isServerStart)
> +{
> + SSL_CTX *ctx;
> + List *sni_hosts = NIL;
> + HostsLine line;
A pointer to `line` is passed down to ssl_init_context(), but it's
only been partially initialized on the stack. Can it be
zero-initialized here instead?
> + if (ssl_snimode == SSL_SNIMODE_STRICT)
> + {
> + ereport(COMMERROR,
> + (errcode(ERRCODE_PROTOCOL_VIOLATION),
> + errmsg("no hostname provided in callback")));
> + return SSL_TLSEXT_ERR_ALERT_FATAL;
> + }
At the moment we're sending an `unrecognized_name` alert in strict
mode if the client doesn't send SNI. RFC 8446 suggests
`missing_extension`:
Additionally, all implementations MUST support the use of the
"server_name" extension with applications capable of using it.
Servers MAY require clients to send a valid "server_name" extension.
Servers requiring this extension SHOULD respond to a ClientHello
lacking a "server_name" extension by terminating the connection with
a "missing_extension" alert.
Should we do that, or should we ignore the suggestion? The problem
with missing_extension, IMO, is that there's absolutely no indication
to the client as to which extension is missing. unrecognized_name is a
little confusing in this case (there was no name sent), but at least
the end user will be able to link that to an SNI problem via search
engine.
> +#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
> + # (change requires restart)
Nitpickiest nitpick: looks like the other lines use a tab instead of a
space between the setting and the trailing comment.
Thanks,
--Jacob
[1] https://postgr.es/m/CAOYmi%2BnTwu7%3DaUGCkf6L-ULqS8itNP7uc9nUmNLOvbXf2TCgBA%40mail.gmail.com
commit a4d9cbf4d1228dcc17f2961b7811321a50e74617
Author: Jacob Champion <[email protected]>
Date: Tue Mar 4 13:17:12 2025 -0800
Tests
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index f0ce048273a..72e64c6c00d 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -33,6 +33,11 @@ if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+
my $node = PostgreSQL::Test::Cluster->new('primary');
$node->init;
@@ -161,4 +166,57 @@ $node->connect_fails(
"connect fails since the passphrase protected key cannot be reloaded",
expected_stderr => qr/tlsv1 unrecognized name/);
+# Test client CAs.
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf', 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf', 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf', 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+my @cases = ( "", "root+client_ca", "root+server_ca" );
+foreach my $default_ca (@cases)
+{
+ # The default CA should, ideally, not matter for the purposes of these
+ # tests, since we connect to the other hosts explicitly.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+ "example.org, $default_ca: connect with sslcert, no client CA configured",
+ expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "example.com, $default_ca: connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/);
+
+ $node->connect_ok(
+ "$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+ "example.com, $default_ca: connect with sslcert, client certificate sent");
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "example.net, $default_ca: connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/);
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+ "example.net, $default_ca: connect with sslcert, client certificate sent",
+ expected_stderr => qr/unknown ca/);
+}
+
done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index e044318531f..bdcce84003e 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -71,6 +71,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -145,7 +146,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -180,10 +182,11 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"
+ if $params->{cafile} ne "";
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
Attachments:
[text/plain] tests.patch.txt (4.5K, 2-tests.patch.txt)
download | inline diff:
commit a4d9cbf4d1228dcc17f2961b7811321a50e74617
Author: Jacob Champion <[email protected]>
Date: Tue Mar 4 13:17:12 2025 -0800
Tests
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index f0ce048273a..72e64c6c00d 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -33,6 +33,11 @@ if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+
my $node = PostgreSQL::Test::Cluster->new('primary');
$node->init;
@@ -161,4 +166,57 @@ $node->connect_fails(
"connect fails since the passphrase protected key cannot be reloaded",
expected_stderr => qr/tlsv1 unrecognized name/);
+# Test client CAs.
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf', 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf', 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf', 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+my @cases = ( "", "root+client_ca", "root+server_ca" );
+foreach my $default_ca (@cases)
+{
+ # The default CA should, ideally, not matter for the purposes of these
+ # tests, since we connect to the other hosts explicitly.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+ "example.org, $default_ca: connect with sslcert, no client CA configured",
+ expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "example.com, $default_ca: connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/);
+
+ $node->connect_ok(
+ "$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+ "example.com, $default_ca: connect with sslcert, client certificate sent");
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "example.net, $default_ca: connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/);
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+ "example.net, $default_ca: connect with sslcert, client certificate sent",
+ expected_stderr => qr/unknown ca/);
+}
+
done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index e044318531f..bdcce84003e 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -71,6 +71,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -145,7 +146,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -180,10 +182,11 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"
+ if $params->{cafile} ne "";
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-05-13 13:46 Andres Freund <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Andres Freund @ 2025-05-13 13:46 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Pgsql Hackers <[email protected]>
Hi,
On 2025-02-27 14:38:24 +0100, Daniel Gustafsson wrote:
> The attached v6 rebase contains this as well as your tests as well as
> general cleanup and comment writing etc.
This is not passing CI on windows...
https://cirrus-ci.com/build/4765059278176256
Greetings,
Andres
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-08-27 19:49 Daniel Gustafsson <[email protected]>
parent: Andres Freund <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-08-27 19:49 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Pgsql Hackers <[email protected]>
> On 13 May 2025, at 15:46, Andres Freund <[email protected]> wrote:
> This is not passing CI on windows...
> https://cirrus-ci.com/build/4765059278176256
When looking into why the SNI tests failed on Windows I think I found a
pre-existing issue that we didn't have tests for, which my patch added tests
for and thus broke.
The test I added was to check restarting and reloading with ssl passphrase
commands (which we do have testcoverage for) with a subsequent connection test
to ensure it didn't just work to start the cluster.
When ssl_passphrase_command_supports_reload is set to 'off', the cluster should
allow connections until a reload has been issued. That works fine except on
Windows where our process-model is such that a new connection will re-run the
passphrase command, which inevitably fails as it's not configured for reload.
The test in my patch exposed this out of (happy) accident, but it can be
reproduced in HEAD as well. The attached version modifies the ssl tests to
cover this with a connection attempt. If I'm not mistaken though, there should
probably be a docs patch to make it clear how this works on Windows.
No codechanges on top of the test fix.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v8-0001-Serverside-SNI-support-for-libpq.patch (56.1K, 2-v8-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 42c1c44ebec355d48755d7821782d49a15568c77 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 2 Jun 2025 10:25:08 +0200
Subject: [PATCH v8] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 ++++
doc/src/sgml/runtime.sgml | 67 ++++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 203 +++++++++-
src/backend/libpq/be-secure-openssl.c | 356 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 26 ++
src/backend/utils/misc/guc_tables.c | 31 ++
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 16 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/001_ssltests.pl | 29 +-
src/test/ssl/t/004_sni.pl | 175 +++++++++
src/test/ssl/t/SSL/Server.pm | 8 +
src/tools/pgindent/typedefs.list | 2 +
23 files changed, 1007 insertions(+), 63 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0a4b3e55ba5..17ba6538de6 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..fa6fe07adc0 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the defualt fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..2d1691c7950 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..67a50c7b24c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,8 +24,13 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +181,196 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ hostcxt = AllocSetContextCreate(PostmasterContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("no SNI configuration added from configuration file \"%s\"",
+ HostsFileName));
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ if (!ok)
+ {
+ MemoryContextDelete(hostcxt);
+ return NIL;
+ }
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index c8b63ef8249..14ff1d78c40 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+ bool ssl_passphrase_support_reload;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +90,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +111,160 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+
+ /*
+ * If there are contexts loaded when we init they must be released.
+ */
+ if (contexts != NIL)
+ {
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate")));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list.
+ */
+ sni_hosts = load_hosts();
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that. The passphrase command fields are
+ * however optional so we need to check whether those were set.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+ if (host->ssl_passphrase_cmd != NULL)
+ host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd);
+ host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded")));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +290,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +308,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +326,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +490,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +572,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -759,6 +921,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1132,7 +1297,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1534,88 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse.
+ */
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * In ssl_snimode "strict" it's an error if there was no match for the
+ * hostname in the TLS extension. Terminate the connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1825,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +2024,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2052,26 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ if (host->ssl_passphrase)
+ pfree(unconstify(char *, host->ssl_passphrase));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..1431f92e332 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 46fdefebe35..ed670822ea3 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -55,6 +55,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ /*
+ * Likewise for pg_hosts.conf
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f137129209f..3339e829314 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -555,6 +562,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4787,6 +4795,17 @@ struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS,
+ gettext_noop("Sets the server's \"hosts\" configuration file."),
+ NULL,
+ GUC_SUPERUSER_ONLY
+ },
+ &HostsFileName,
+ NULL,
+ NULL, NULL, NULL
+ },
+
{
{"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS,
gettext_noop("Writes the postmaster PID to the specified file."),
@@ -5457,6 +5476,18 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Sets the SNI mode to use."),
+ NULL,
+ GUC_SUPERUSER_ONLY,
+ },
+ &ssl_snimode,
+ SSL_SNIMODE_DEFAULT,
+ ssl_snimode_options,
+ NULL, NULL, NULL
+ },
+
{
{"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS,
gettext_noop("Sets the method for synchronizing the data directory before crash recovery."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a9d8293474a..57b1be3c38f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..087cea4fffc 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,13 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n"
+ "PG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2830,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 3657f182db3..3d8e33533b8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index aeb66ca40cf..5feed0eb0a4 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -134,12 +135,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f619100467d..025e7e95e90 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -288,6 +288,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..a46ac325045 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches agsinst the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b2eb18d3e81..0f0f64b6c7c 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -51,8 +51,15 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
my $supports_sslcertmode_require =
check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
-# Allocation of base connection string shared among multiple tests.
-my $common_connstr;
+# Set of default settings for SSL parameters in connection string. This
+# makes the tests protected against any defaults the environment may have
+# in ~/.postgresql/.
+my $default_ssl_connstr =
+ "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
+
+# Base connection string shared among multiple tests.
+my $common_connstr =
+ "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
#### Set up the server.
@@ -85,7 +92,7 @@ switch_server_cert(
passphrase_cmd => 'echo wrongpassword',
restart => 'no');
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(fail_ok => 1, log_like => qr/could not load private key file/);
is($result, 0,
'restart fails with password-protected key file with wrong password');
@@ -95,11 +102,16 @@ switch_server_cert(
cafile => 'root+client_ca',
keyfile => 'server-password',
passphrase_cmd => 'echo secret1',
+ passphrase_cmd_reload => 'yes',
restart => 'no');
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(fail_ok => 1, log_unlike => qr/could not load private key file/);
is($result, 1, 'restart succeeds with password-protected key file');
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
# Test compatibility of SSL protocols.
# TLSv1.1 is lower than TLSv1.2, so it won't work.
$node->append_conf(
@@ -139,15 +151,6 @@ note "running client tests";
switch_server_cert($node, certfile => 'server-cn-only');
-# Set of default settings for SSL parameters in connection string. This
-# makes the tests protected against any defaults the environment may have
-# in ~/.postgresql/.
-my $default_ssl_connstr =
- "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
-
-$common_connstr =
- "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
-
SKIP:
{
skip "Keylogging is not supported with LibreSSL", 5 if $libressl;
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..b3b2821a2bd
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,175 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=strict",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded");
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index efbd0dafaf6..31e50c8722f 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -296,6 +296,11 @@ The CRL directory to use. Implementation is SSL backend specific.
The passphrase command to use. If not set, an empty passphrase command will
be set.
+=item passphrase_cmd_reload => B<value>
+
+Whether or not to allow passphrase command reloading. If set the passphrase
+command will set to allow reloading.
+
=item restart => B<value>
If set to 'no', the server won't be restarted after updating the settings.
@@ -327,6 +332,9 @@ sub switch_server_cert
"ssl_passphrase_command='" . $params{passphrase_cmd} . "'")
if defined $params{passphrase_cmd};
+ $node->append_conf('sslconfig.conf', 'ssl_passphrase_command_supports_reload=on')
+ if defined $params{passphrase_cmd_reload};
+
return if (defined($params{restart}) && $params{restart} eq 'no');
$node->restart;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..5b61b9a1d4d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1202,6 +1202,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-09-01 01:58 Michael Paquier <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2025-09-01 01:58 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Andres Freund <[email protected]>; Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, Aug 27, 2025 at 09:49:34PM +0200, Daniel Gustafsson wrote:
> When looking into why the SNI tests failed on Windows I think I found a
> pre-existing issue that we didn't have tests for, which my patch added tests
> for and thus broke.
>
> The test I added was to check restarting and reloading with ssl passphrase
> commands (which we do have testcoverage for) with a subsequent connection test
> to ensure it didn't just work to start the cluster.
Would this part be better if extracted from the main patch and then
backpatched? Even if not backpatched, a split would be cleaner on
HEAD, I assume, leading to less fuzz with the main patch.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, 2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-09-02 12:48 Daniel Gustafsson <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-09-02 12:48 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Andres Freund <[email protected]>; Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
> On 1 Sep 2025, at 03:58, Michael Paquier <[email protected]> wrote:
>
> On Wed, Aug 27, 2025 at 09:49:34PM +0200, Daniel Gustafsson wrote:
>> When looking into why the SNI tests failed on Windows I think I found a
>> pre-existing issue that we didn't have tests for, which my patch added tests
>> for and thus broke.
>>
>> The test I added was to check restarting and reloading with ssl passphrase
>> commands (which we do have testcoverage for) with a subsequent connection test
>> to ensure it didn't just work to start the cluster.
>
> Would this part be better if extracted from the main patch and then
> backpatched? Even if not backpatched, a split would be cleaner on
> HEAD, I assume, leading to less fuzz with the main patch.
Yes, that's my plan, just wanted to float it here first to see if I was
thinking about it all wrong. I will raise it on its own thread on -hackers.
The backpatchable portion is probably limited to a docs entry clarifying the
behaviour on Windows.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-10 22:32 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2025-11-10 22:32 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Andres Freund <[email protected]>; Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
Attached is a cleaned up rebase with improved memory handling, additional code
documentation, removed passphrase test (sent as a separate thread), and some
general cleanup and additional testing.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v9-0001-Serverside-SNI-support-for-libpq.patch (54.3K, 2-v9-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 542ffd45c914597821f258e4b838371e03abc32e Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 2 Jun 2025 10:25:08 +0200
Subject: [PATCH v9] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 +++
doc/src/sgml/runtime.sgml | 67 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 204 +++++++++-
src/backend/libpq/be-secure-openssl.c | 382 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 15 +
src/backend/utils/misc/guc_tables.c | 8 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 175 ++++++++
src/tools/pgindent/typedefs.list | 2 +
22 files changed, 1005 insertions(+), 51 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 683f7c36f46..ed6d42a1561 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..2693dcb81ad 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the default fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..2d1691c7950 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..2d9baff92b5 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,193 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("SNI configuration not found in configuration file \"%s\"",
+ HostsFileName));
+ return NIL;
+ }
+
+ if (!ok)
+ return NIL;
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index c8b63ef8249..ea3ba012b86 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,17 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +81,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +89,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
@@ -96,11 +110,173 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (contexts != NIL)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate"));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ hostcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list. Make sure to allocate the parsed rows in a temporary
+ * memory context such that we can avoid memory leaks.
+ */
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+ sni_hosts = load_hosts();
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that. The passphrase command fields are
+ * however optional so we need to check whether those were set.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca)
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+
+ MemoryContextDelete(hostcxt);
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -126,10 +302,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -137,16 +320,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -155,19 +338,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -319,17 +502,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -401,38 +584,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -759,6 +933,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1132,7 +1309,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1369,6 +1546,104 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Depending on the SNI mode we either
+ * require a perfect match, or we allow to fallback to a default configuration.
+ * Returning SSL_TLSEXT_ERR_ALERT_FATAL to OpenSSL will immediately terminate
+ * the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse. Throw an assertion to catch during testing
+ * but also ensure to terminate the connection in non-assert builds, even
+ * though this should never happen, just to be on the safe side.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF)
+ {
+ Assert(false);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * At this point we know that the requested hostname isn't configured in
+ * the pg_hosts file. In ssl_snimode "strict" it's an error if there was
+ * no match for the hostname in the TLS extension so terminate the
+ * connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1853,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1771,17 +2052,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1793,3 +2080,24 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..1431f92e332 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 679846da42c..ec92c0739b3 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 1128167c025..186ac634dae 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1160,6 +1160,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
@@ -2735,6 +2742,14 @@
max => '0',
},
+{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets the SNI mode to use for the server.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_snimode',
+ boot_val => 'SSL_SNIMODE_DEFAULT',
+ options => 'ssl_snimode_options',
+},
+
{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.',
long_desc => 'An empty string means use the default cipher suites.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0209b2067a2..d429c658054 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -556,6 +563,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f62b61967ef..5828516c4e3 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..c953f24a58d 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2829,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e3748d3c8c9..c96818549cc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 5af005ad779..fe2d431291a 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -152,12 +153,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da89..8f08a38b789 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..a46ac325045 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches agsinst the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..b3b2821a2bd
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,175 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=strict",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded");
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 432509277c9..fc66acb2f12 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1206,6 +1206,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-11 09:06 Chao Li <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Chao Li @ 2025-11-11 09:06 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Jacob Champion <[email protected]>; Pgsql Hackers <[email protected]>
Hi Daniel,
I just reviewed the patch and got a few comments:
> On Nov 11, 2025, at 06:32, Daniel Gustafsson <[email protected]> wrote:
>
> Attached is a cleaned up rebase with improved memory handling, additional code
> documentation, removed passphrase test (sent as a separate thread), and some
> general cleanup and additional testing.
>
> --
> Daniel Gustafsson
>
> <v9-0001-Serverside-SNI-support-for-libpq.patch>
1 - commit message
```
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
```
Typo: certicate -> certificate
2 - be-secure-common.c
```
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
```
Cmd is only passed to replace_percent_placeholders(), and the function take a "const char *” argument, so we can define cmd as “const char *”.
2 - be-secure-common.c
```
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
```
When I read this function, I thought to raise a comment for freeing hosts_lines. However, then I read be-secure-openssl.c, I saw the load_hosts() is called within a transient hostctx, so it doesn’t have to free memory pieces. Can we explain that in the function comment? Otherwise other reviewers and future code readers may have the same confusion.
3 - be-secure-openssl.c
```
int
@@ -759,6 +933,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
```
Do we need to free_contexts() here? When be_tls_init() is called again, contexts will anyway be freed, so I guess earlier free might be better?
4 - guc_parameters.dat
```
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets the SNI mode to use for the server.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_snimode',
+ boot_val => 'SSL_SNIMODE_DEFAULT',
+ options => 'ssl_snimode_options',
+},
```
If ssl_snimode is PGC_SIGHUP that allows to reload without a server reset, why hosts_file cannot?
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-12 22:44 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 2 replies; 58+ messages in thread
From: Jacob Champion @ 2025-11-12 22:44 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Mon, Nov 10, 2025 at 2:33 PM Daniel Gustafsson <[email protected]> wrote:
> Attached is a cleaned up rebase with improved memory handling, additional code
> documentation, removed passphrase test (sent as a separate thread), and some
> general cleanup and additional testing.
Thanks! Builds and passes back to OpenSSL 1.1.1 and LibreSSL 3.4
(except for the unrelated known issue with "depth 0"/"depth 1", which
this patch did not introduce [1]).
Did you have any thoughts on my earlier review [2]? The test patch
attached there still fails on my machine with v9.
Thanks,
--Jacob
[1] https://postgr.es/m/CA%2BhUKG%2BfLqyweHqFSBcErueUVT0vDuSNWui-ySz3%2Bd_APmq7dw%40mail.gmail.com
[1] https://postgr.es/m/CAOYmi%2Bk%3DVF-2BCqfR49A92tx%3D_QNuL%3D3iT3w6FysOffKw9cxDQ%40mail.gmail.com
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-12 22:50 Jacob Champion <[email protected]>
parent: Chao Li <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Jacob Champion @ 2025-11-12 22:50 UTC (permalink / raw)
To: Chao Li <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Tue, Nov 11, 2025 at 1:07 AM Chao Li <[email protected]> wrote:
> If ssl_snimode is PGC_SIGHUP that allows to reload without a server reset, why hosts_file cannot?
I think all of our FILE_LOCATIONS GUCs are handled similarly.
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-12 23:03 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2025-11-12 23:03 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 12 Nov 2025, at 23:44, Jacob Champion <[email protected]> wrote:
> Did you have any thoughts on my earlier review [2]? The test patch
> attached there still fails on my machine with v9.
Oh shoot, I missed that when going back over the thread. Will have a look.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-24 14:53 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-11-24 14:53 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 12 Nov 2025, at 23:44, Jacob Champion <[email protected]> wrote:
> Did you have any thoughts on my earlier review [2]? The test patch
> attached there still fails on my machine with v9.
The attached incorporates your tests, fixes them to make them pass. The
culprit seemed to be a combination of a bug in the code (the verify callback
need to be defined in the default context even if there is no CA for it to be
called in an SNI setting because OpenSSL), and that the tests were matching
backend errors against frontend messages.
The other comments from your review are also addressed, as well as additional
cleanup and improved error handling.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v10-0001-Serverside-SNI-support-for-libpq.patch (60.5K, 2-v10-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 11984e057105c89d7f02f8f0be2a736ababee1f1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 24 Nov 2025 15:52:27 +0100
Subject: [PATCH v10] Serverside SNI support for libpq
Experimental support for serverside SNI support in libpq, a new config
file $datadir/pg_hosts.conf is used for configuring which certicate and
key should be used for which hostname. A new GUC, ssl_snimode, is added
which controls how the hostname TLS extension is handled. The possible
values are off, default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. The normal SSL GUCs for certificates and keys
are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 +++
doc/src/sgml/runtime.sgml | 67 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 204 ++++++++-
src/backend/libpq/be-secure-openssl.c | 430 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 15 +
src/backend/utils/misc/guc_tables.c | 8 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 237 ++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 +-
src/tools/pgindent/typedefs.list | 2 +
23 files changed, 1120 insertions(+), 62 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 023b3f03ba9..8bca363d542 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..2693dcb81ad 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the default fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..2d1691c7950 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..2d9baff92b5 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ char *cmd = (char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,193 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
+ * of errors.
+ */
+List *
+load_hosts(void)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return NIL;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+
+ /*
+ * If we didn't find any SNI configuration then that's not an error since
+ * the pg_hosts file is additive to the default SSL configuration.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("SNI configuration not found in configuration file \"%s\"",
+ HostsFileName));
+ return NIL;
+ }
+
+ if (!ok)
+ return NIL;
+
+ return parsed_lines;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..38134ec87de 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,17 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +81,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +89,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
struct CallbackErr
{
@@ -102,11 +116,174 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (contexts != NIL)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ memset(&line, 0, sizeof(line));
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate"));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ hostcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list. Make sure to allocate the parsed rows in a temporary
+ * memory context such that we can avoid memory leaks.
+ */
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+ sni_hosts = load_hosts();
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"),
+ errhint("In strict ssl_snimode there need to be at least one entry in pg_hosts.conf."));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca && host->ssl_ca[0] != '\0')
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+
+ MemoryContextDelete(hostcxt);
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -132,10 +309,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -143,16 +327,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -161,19 +345,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -325,17 +509,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -348,16 +532,20 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
+ }
+
+ /*
+ * If we have a CA store, or SNI is enabled, always ask for SSL client
+ * cert, but don't fail if it's not presented. We might fail such
+ * connections later, depending on what we find in pg_hba.conf. The reason
+ * for enabling in the case of SNI even if there is no CA is that another
+ * context might have a CA, so the callback must be installed in order for
+ * that context.
+ */
+ if (ctx_ssl_ca_file[0] || ssl_snimode != SSL_SNIMODE_OFF)
SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
verify_cb);
- }
/*----------
* Load the Certificate Revocation List (CRL).
@@ -407,38 +595,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -771,6 +950,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1144,7 +1326,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1390,6 +1572,131 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Depending on the SNI mode we either
+ * require a perfect match, or we allow to fallback to a default configuration.
+ * Returning SSL_TLSEXT_ERR_ALERT_FATAL to OpenSSL will immediately terminate
+ * the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse. Throw an assertion to catch during testing
+ * but also ensure to terminate the connection in non-assert builds, even
+ * though this should never happen, just to be on the safe side.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF)
+ {
+ Assert(false);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch SSL context for SNI host"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * At this point we know that the requested hostname isn't configured in
+ * the pg_hosts file. In ssl_snimode "strict" it's an error if there was
+ * no match for the hostname in the TLS extension so terminate the
+ * connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1599,6 +1906,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1792,17 +2105,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1814,3 +2133,24 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..1431f92e332 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_DEFAULT;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c6484aea087..f35a15b48df 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 1128167c025..186ac634dae 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1160,6 +1160,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
@@ -2735,6 +2742,14 @@
max => '0',
},
+{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets the SNI mode to use for the server.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_snimode',
+ boot_val => 'SSL_SNIMODE_DEFAULT',
+ options => 'ssl_snimode_options',
+},
+
{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.',
long_desc => 'An empty string means use the default cipher suites.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0209b2067a2..d429c658054 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -556,6 +563,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..4d0722e7bd7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..c953f24a58d 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2829,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e3748d3c8c9..c96818549cc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 5af005ad779..fe2d431291a 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -152,12 +153,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern List *load_hosts(void);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da89..8f08a38b789 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..a46ac325045 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches agsinst the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..90d7560ad11
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,237 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=strict",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv13 alert missing extension/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after more reloads");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded");
+
+# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
+# time swapping out default contexts containing different CA configurations.
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+foreach my $default_ca ("", "root+client_ca", "root+server_ca")
+{
+ # The default CA should, not matter for the purposes of these tests, since
+ # we connect to the other hosts explicitly. Test with various default CA
+ # settings to ensure it's isolated from the actual connections.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
+ expected_stderr => qr/certificate verify failed/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
+ );
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 4159addb700..6ea5b64dc28 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n"
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57a8f0366a5..bd80880c453 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1206,6 +1206,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-24 23:28 Chao Li <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Chao Li @ 2025-11-24 23:28 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Hi Daniel,
None of my comment on v9 are addressed in v10:
>
> 1 - commit message
> ```
> Experimental support for serverside SNI support in libpq, a new config
> file $datadir/pg_hosts.conf is used for configuring which certicate and
> ```
>
> Typo: certicate -> certificate
>
> 2 - be-secure-common.c
> ```
> +run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
> {
> int loglevel = is_server_start ? ERROR : LOG;
> char *command;
> FILE *fh;
> int pclose_rc;
> size_t len = 0;
> + char *cmd = (char *) userdata;
> ```
>
> Cmd is only passed to replace_percent_placeholders(), and the function take a "const char *” argument, so we can define cmd as “const char *”.
>
> 2 - be-secure-common.c
> ```
> + tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
> +
> + foreach(line, hosts_lines)
> + {
> + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
> +
> + if (tok_line->err_msg != NULL)
> + {
> + ok = false;
> + continue;
> + }
> +
> + if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
> + {
> + ok = false;
> + continue;
> + }
> +
> + parsed_lines = lappend(parsed_lines, newline);
> + }
> +
> + free_auth_file(file, 0);
> ```
>
> When I read this function, I thought to raise a comment for freeing hosts_lines. However, then I read be-secure-openssl.c, I saw the load_hosts() is called within a transient hostctx, so it doesn’t have to free memory pieces. Can we explain that in the function comment? Otherwise other reviewers and future code readers may have the same confusion.
>
> 3 - be-secure-openssl.c
> ```
> int
> @@ -759,6 +933,9 @@ be_tls_close(Port *port)
> pfree(port->peer_dn);
> port->peer_dn = NULL;
> }
> +
> + Host_context = NULL;
> + SSL_context = NULL;
> }
> ```
>
> Do we need to free_contexts() here? When be_tls_init() is called again, contexts will anyway be freed, so I guess earlier free might be better?
>
> 4 - guc_parameters.dat
> ```
> +{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
> + short_desc => 'Sets the server\'s "hosts" configuration file.',
> + flags => 'GUC_SUPERUSER_ONLY',
> + variable => 'HostsFileName',
> + boot_val => 'NULL',
> +},
>
> +{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
> + short_desc => 'Sets the SNI mode to use for the server.',
> + flags => 'GUC_SUPERUSER_ONLY',
> + variable => 'ssl_snimode',
> + boot_val => 'SSL_SNIMODE_DEFAULT',
> + options => 'ssl_snimode_options',
> +},
> ```
>
> If ssl_snimode is PGC_SIGHUP that allows to reload without a server reset, why hosts_file cannot?
Comment 4 can be ignored as Jacob has answered.
> On Nov 24, 2025, at 22:53, Daniel Gustafsson <[email protected]> wrote:
>
>> On 12 Nov 2025, at 23:44, Jacob Champion <[email protected]> wrote:
>
>> Did you have any thoughts on my earlier review [2]? The test patch
>> attached there still fails on my machine with v9.
>
> The attached incorporates your tests, fixes them to make them pass. The
> culprit seemed to be a combination of a bug in the code (the verify callback
> need to be defined in the default context even if there is no CA for it to be
> called in an SNI setting because OpenSSL), and that the tests were matching
> backend errors against frontend messages.
>
> The other comments from your review are also addressed, as well as additional
> cleanup and improved error handling.
>
> --
> Daniel Gustafsson
>
> <v10-0001-Serverside-SNI-support-for-libpq.patch>
I reviewed v10 again, and got some a few more comments:
5 - runtime.sgml
```
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
```
In the patch code, default context uses hostname “*”, should we explain “*” here in the doc?
6 - runtime.sgml
```
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
```
Typo: clusters => cluster’s
7 - runtime.sgml
```
+ will only be used to for the handshake until the hostname is inspected, it
```
“Used to for” => “used for"
8 - Cluster.pm
```
+matching the specified pattern. If the pattern matches agsinst the logfile a
```
Typo: agsinst => against
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-25 14:39 Daniel Gustafsson <[email protected]>
parent: Chao Li <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-11-25 14:39 UTC (permalink / raw)
To: Chao Li <[email protected]>; +Cc: Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 25 Nov 2025, at 00:28, Chao Li <[email protected]> wrote:
>
> Hi Daniel,
>
> None of my comment on v9 are addressed in v10:
I do apologise, I was so focused on fixing Jacob's tests that I forgot about
addressing these. Please find the attached v11 with your comments addressed.
Thank you for all your review, much appreciated!
>> 1 - commit message
>> ```
>> Experimental support for serverside SNI support in libpq, a new config
>> file $datadir/pg_hosts.conf is used for configuring which certicate and
>> ```
>>
>> Typo: certicate -> certificate
Fixed. I also reworded the commit message from saying experimental since we
don't have a concept of experimental features really.
>> 2 - be-secure-common.c
>> ```
>> +run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
>> {
>> int loglevel = is_server_start ? ERROR : LOG;
>> char *command;
>> FILE *fh;
>> int pclose_rc;
>> size_t len = 0;
>> + char *cmd = (char *) userdata;
>> ```
>>
>> Cmd is only passed to replace_percent_placeholders(), and the function take a "const char *” argument, so we can define cmd as “const char *”.
Fixed.
>> 2 - be-secure-common.c
>> ```
>> + tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
>> +
>> + foreach(line, hosts_lines)
>> + {
>> + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
>> +
>> + if (tok_line->err_msg != NULL)
>> + {
>> + ok = false;
>> + continue;
>> + }
>> +
>> + if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
>> + {
>> + ok = false;
>> + continue;
>> + }
>> +
>> + parsed_lines = lappend(parsed_lines, newline);
>> + }
>> +
>> + free_auth_file(file, 0);
>> ```
>>
>> When I read this function, I thought to raise a comment for freeing hosts_lines. However, then I read be-secure-openssl.c, I saw the load_hosts() is called within a transient hostctx, so it doesn’t have to free memory pieces. Can we explain that in the function comment? Otherwise other reviewers and future code readers may have the same confusion.
I expanded the comment, and while there also improved the error reporting from
the function by returning a bool indicating status as well as the list (since
NIL was both empty-file and error).
>> 3 - be-secure-openssl.c
>> ```
>> int
>> @@ -759,6 +933,9 @@ be_tls_close(Port *port)
>> pfree(port->peer_dn);
>> port->peer_dn = NULL;
>> }
>> +
>> + Host_context = NULL;
>> + SSL_context = NULL;
>> }
>> ```
>>
>> Do we need to free_contexts() here? When be_tls_init() is called again, contexts will anyway be freed, so I guess earlier free might be better?
I don't think so, be_tls_close is only for closing the session.
> I reviewed v10 again, and got some a few more comments:
>
> 5 - runtime.sgml
> ```
> + in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
> + is matched against the hostname TLS extension in the SSL handshake.
> ```
>
> In the patch code, default context uses hostname “*”, should we explain “*” here in the doc?
I don't think we should since we don't want anyone to configure a host with
'*'. That does bring up a good point though, and I added a check in the
parsing to ensure that such wildcard hostnames cause failures in parsing if
found in pg_hosts.
> 6 - runtime.sgml
> ```
> + <filename>pg_hosts.conf</filename>, which is stored in the clusters
> + data directory. The hosts configuration file contains lines of the general
> ```
>
> Typo: clusters => cluster’s
Fixed.
> 7 - runtime.sgml
> ```
> + will only be used to for the handshake until the hostname is inspected, it
> ```
>
> “Used to for” => “used for"
Fixed.
> 8 - Cluster.pm
> ```
> +matching the specified pattern. If the pattern matches agsinst the logfile a
> ```
>
> Typo: agsinst => against
Fixed.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v11-0001-Serverside-SNI-support-for-libpq.patch (62.2K, 2-v11-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From dee74be3d7d00363a444e6c56d60d3daa5abdda8 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 25 Nov 2025 15:33:18 +0100
Subject: [PATCH v11] Serverside SNI support for libpq
Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certficate/key handling
is available per host. A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname. A new GUC, ssl_snimode, is used to control how the hostname
TLS extension is handled. The possible values are off (which is used
as the new backwards compatible default), default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. SSL GUCs for certificates and keys are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 +++
doc/src/sgml/runtime.sgml | 67 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 223 ++++++++-
src/backend/libpq/be-secure-openssl.c | 451 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 15 +
src/backend/utils/misc/guc_tables.c | 8 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 237 +++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 +-
src/tools/pgindent/typedefs.list | 2 +
23 files changed, 1160 insertions(+), 62 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 023b3f03ba9..8bca363d542 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..687aa86f68a 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the cluster's
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the default fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..2d1691c7950 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..8d5a0c1ba2c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ const char *cmd = (const char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,212 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+ if (strcmp(parsedline->hostname, "*") == 0)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("wildcard hostname not allowed in hosts configuration"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file. The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false. or an empty file. This
+ * function is intended to be executed within a temporary memory context which
+ * can be discarded to free memory allocated during the processing of the file.
+ */
+bool
+load_hosts(List **hosts)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ if (hosts)
+ *hosts = NIL;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return false;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+
+ /*
+ * If we didn't find any SNI configuration then that might not be an error
+ * since the pg_hosts file is additive to the default SSL configuration in
+ * some ssl_sni settings.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("SNI configuration not found in configuration file \"%s\"",
+ HostsFileName));
+ }
+
+ if (!ok)
+ return false;
+
+ if (hosts)
+ *hosts = parsed_lines;
+
+ return true;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..ac3984bc275 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,17 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +81,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +89,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
struct CallbackErr
{
@@ -102,11 +116,195 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+ bool res;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (contexts != NIL)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ memset(&line, 0, sizeof(line));
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate"));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the hand- shake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ hostcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list. Make sure to allocate the parsed rows in a temporary
+ * memory context such that we can avoid memory leaks.
+ */
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+ res = load_hosts(&sni_hosts);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If loading failed, then make sure to error out regardless. It's not
+ * really an error to not have a hosts file in non-strict modes but if
+ * there is one and it fails to load properly, then silently pressing
+ * on seems worse than raising an error.
+ */
+ if (!res)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"),
+ errhint("In strict ssl_snimode there need to be at least one entry in pg_hosts.conf."));
+ return -1;
+ }
+
+ /*
+ * Loading and parsing the hosts file was successful, create contexts
+ * for each host entry and add to the the list of host to be checked
+ * during login.
+ */
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca && host->ssl_ca[0] != '\0')
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+
+ MemoryContextDelete(hostcxt);
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -132,10 +330,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -143,16 +348,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -161,19 +366,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -325,17 +530,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -348,16 +553,20 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
+ }
+
+ /*
+ * If we have a CA store, or SNI is enabled, always ask for SSL client
+ * cert, but don't fail if it's not presented. We might fail such
+ * connections later, depending on what we find in pg_hba.conf. The reason
+ * for enabling in the case of SNI even if there is no CA is that another
+ * context might have a CA, so the callback must be installed in order for
+ * that context.
+ */
+ if (ctx_ssl_ca_file[0] || ssl_snimode != SSL_SNIMODE_OFF)
SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
verify_cb);
- }
/*----------
* Load the Certificate Revocation List (CRL).
@@ -407,38 +616,29 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
+
SSL_context = NULL;
- ssl_loaded_verify_locations = false;
}
int
@@ -771,6 +971,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1144,7 +1347,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1390,6 +1593,131 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Depending on the SNI mode we either
+ * require a perfect match, or we allow to fallback to a default configuration.
+ * Returning SSL_TLSEXT_ERR_ALERT_FATAL to OpenSSL will immediately terminate
+ * the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse. Throw an assertion to catch during testing
+ * but also ensure to terminate the connection in non-assert builds, even
+ * though this should never happen, just to be on the safe side.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF)
+ {
+ Assert(false);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch SSL context for SNI host"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * At this point we know that the requested hostname isn't configured in
+ * the pg_hosts file. In ssl_snimode "strict" it's an error if there was
+ * no match for the hostname in the TLS extension so terminate the
+ * connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1599,6 +1927,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1792,17 +2126,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1814,3 +2154,24 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..f6c1422b555 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_OFF;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c6484aea087..f35a15b48df 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 1128167c025..a895067f7ad 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1160,6 +1160,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
@@ -2735,6 +2742,14 @@
max => '0',
},
+{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets the SNI mode to use for the server.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_snimode',
+ boot_val => 'SSL_SNIMODE_OFF',
+ options => 'ssl_snimode_options',
+},
+
{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.',
long_desc => 'An empty string means use the default cipher suites.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0209b2067a2..d429c658054 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -556,6 +563,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..c5ff8302201 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = off
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..c953f24a58d 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2829,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e3748d3c8c9..c96818549cc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 5af005ad779..54d8ee8a2aa 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -152,12 +153,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern bool load_hosts(List **hosts);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da89..8f08a38b789 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..55e0f04d4f5 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches against the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..90d7560ad11
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,237 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=strict",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv13 alert missing extension/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after more reloads");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded");
+
+# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
+# time swapping out default contexts containing different CA configurations.
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+foreach my $default_ca ("", "root+client_ca", "root+server_ca")
+{
+ # The default CA should, not matter for the purposes of these tests, since
+ # we connect to the other hosts explicitly. Test with various default CA
+ # settings to ensure it's isolated from the actual connections.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
+ expected_stderr => qr/certificate verify failed/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
+ );
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 4159addb700..bbd3bed6c86 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57a8f0366a5..bd80880c453 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1206,6 +1206,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Re: Serverside SNI support in libpq
@ 2025-11-26 09:14 Dewei Dai <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Dewei Dai @ 2025-11-26 09:14 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; li.evan.chao <[email protected]>; +Cc: Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Hi Daniel,
I just reviewed the v11 patch and got a few comments:
1 - commit message
```This adds support for serverside SNI such that certficate/key handling
```
Typo: certficate -> certificate
2 -be-secure-openssl.c
```* host/snimode match, but we need something to drive the hand- shake till
```
Typo: hand- shake ->handshake
3 - be-secure-openssl.c
```
errhint("In strict ssl_snimode there need to be at least one entry in pg_hosts.conf."));
there needs to be
```
Typo: There need to be -> there needs to be
4 - src/backend/makefile
It is recommended to delete pg_hosts.conf.sample during the `make uninstall` command
5 - be-secure-openssl.c
```
be_tls_destroy(void)
{
+ ListCell *cell;
+
+ foreach(cell, contexts)
+ {
+ HostContext *host_context = lfirst(cell);
+
+ SSL_CTX_free(host_context->context);
+ pfree(host_context);
+ }
`````
In the `be_tls_destroy` function, the context is released, but it is not set to null.
This is similar to the `free_context` function, and it seems that it can be called directly.
Best regards
[email protected]
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-11-26 14:33 Daniel Gustafsson <[email protected]>
parent: Dewei Dai <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-11-26 14:33 UTC (permalink / raw)
To: Dewei Dai <[email protected]>; +Cc: li.evan.chao <[email protected]>; Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 26 Nov 2025, at 10:14, Dewei Dai <[email protected]> wrote:
>
> Hi Daniel,
> I just reviewed the v11 patch and got a few comments:
Thanks!
> Typo: certficate -> certificate
Fixed.
> Typo: hand- shake ->handshake
Fixed.
> Typo: There need to be -> there needs to be
AFAIK "need to be" is the correct spelling for referring to a singular thing,
and "needs to be" is correct for plural. I've been thinking about this in a
singular context but maybe "needs to be" is the right wording since the hint is
"at least one". Changed to "needs to be" just in case.
> It is recommended to delete pg_hosts.conf.sample during the `make uninstall` command
Nice catch, fixed.
> In the `be_tls_destroy` function, the context is released, but it is not set to null.
> This is similar to the `free_context` function, and it seems that it can be called directly.
That's a good point, be_tls_destroy can just call free_contexts directly and
save some code while making sure it's consistent. Fixed.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v12-0001-Serverside-SNI-support-for-libpq.patch (62.4K, 2-v12-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From d3eecf4ec13de48e8729c92670560ce79a15b6d1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 26 Nov 2025 15:30:56 +0100
Subject: [PATCH v12] Serverside SNI support for libpq
Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certificate/key handling
is available per host. A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname. A new GUC, ssl_snimode, is used to control how the hostname
TLS extension is handled. The possible values are off (which is used
as the new backwards compatible default), default and strict:
- off: pg_hosts.conf is not parsed and the hostname TLS extension is
not inspected at all. SSL GUCs for certificates and keys are used.
- default: pg_hosts.conf is loaded as well as the normal GUCs. If no
match for the TLS extension hostname is found in pg_hosts the cert
and key from the postgresql.conf GUCs is used as the default (used
as a wildcard host).
- strict: only pg_hosts.conf is loaded and the TLS extension hostname
MUST be passed and MUST have a match in the configuration, else the
connection is refused.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Dewei Dai <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/config.sgml | 66 +++
doc/src/sgml/runtime.sgml | 67 +++
src/backend/Makefile | 2 +
src/backend/libpq/be-secure-common.c | 223 ++++++++-
src/backend/libpq/be-secure-openssl.c | 443 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 15 +
src/backend/utils/misc/guc_tables.c | 8 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 19 +
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 11 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 237 ++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 +-
src/tools/pgindent/typedefs.list | 2 +
23 files changed, 1152 insertions(+), 63 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 737b90736bf..acae1601d39 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1700,6 +1700,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..687aa86f68a 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the cluster's
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the default fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..529126eebeb 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
@@ -246,6 +247,7 @@ endif
$(MAKE) -C utils uninstall-data
rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \
'$(DESTDIR)$(datadir)/pg_ident.conf.sample' \
+ '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \
'$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
$(call uninstall_llvm_module,postgres)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..8d5a0c1ba2c 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ const char *cmd = (const char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,212 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+ if (strcmp(parsedline->hostname, "*") == 0)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("wildcard hostname not allowed in hosts configuration"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file. The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false. or an empty file. This
+ * function is intended to be executed within a temporary memory context which
+ * can be discarded to free memory allocated during the processing of the file.
+ */
+bool
+load_hosts(List **hosts)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ if (hosts)
+ *hosts = NIL;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, NULL);
+ if (file == NULL)
+ {
+ /* An error has already been logged so no need to add one here */
+ return false;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if (tok_line->err_msg != NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+
+ /*
+ * If we didn't find any SNI configuration then that might not be an error
+ * since the pg_hosts file is additive to the default SSL configuration in
+ * some ssl_sni settings.
+ */
+ if (ok && parsed_lines == NIL)
+ {
+ ereport(DEBUG1,
+ errmsg("SNI configuration not found in configuration file \"%s\"",
+ HostsFileName));
+ }
+
+ if (!ok)
+ return false;
+
+ if (hosts)
+ *hosts = parsed_lines;
+
+ return true;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..678d1587e44 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,17 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ const char *ssl_passphrase;
+ SSL_CTX *context;
+ bool default_host;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +81,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +89,17 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+static List *contexts = NIL;
static SSL_CTX *SSL_context = NULL;
+static HostContext *Default_context = NULL;
+static HostContext *Host_context = NULL;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
struct CallbackErr
{
@@ -102,11 +116,195 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+ HostsLine line;
+ bool res;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (contexts != NIL)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ Default_context = NULL;
+ }
+
+ /*
+ * Load the default configuration from postgresql.conf such that we have a
+ * context to either be used for the entire connection, or drive the
+ * handshake until the SNI callback replace it with a configuration from
+ * the pg_hosts.conf file.
+ */
+ memset(&line, 0, sizeof(line));
+ line.ssl_cert = ssl_cert_file;
+ line.ssl_key = ssl_key_file;
+ line.ssl_ca = ssl_ca_file;
+ line.ssl_passphrase_cmd = ssl_passphrase_command;
+ line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ ctx = ssl_init_context(isServerStart, &line);
+ if (ctx == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load default certificate"));
+ return -1;
+ }
+
+ Default_context = palloc0(sizeof(HostContext));
+ Default_context->hostname = pstrdup("*");
+ Default_context->context = ctx;
+ Default_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into SSL_context.
+ */
+ if (ssl_ca_file[0])
+ Default_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * While the default context isn't matched against when searching for host
+ * contexts we still add it to the list to ensure that cleanup code can
+ * iterate over a single structure to clean up everything.
+ */
+ contexts = lappend(contexts, Default_context);
+
+ /*
+ * Install the default context to use as the initial context for the
+ * connection. This might be replaced in the SNI callback if there is a
+ * host/snimode match, but we need something to drive the handshake till
+ * then.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+
+ /*
+ * In default or strict ssl_snimode we load all certificates/keys which
+ * are configured in pg_hosts.conf. In strict mode it is considered a
+ * fatal error in case there are no configured entries.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext hostcxt;
+
+ hostcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+
+ /*
+ * Load pg_hosts.conf and parse each row, returning the set of hosts
+ * as a list. Make sure to allocate the parsed rows in a temporary
+ * memory context such that we can avoid memory leaks.
+ */
+ oldcxt = MemoryContextSwitchTo(hostcxt);
+ res = load_hosts(&sni_hosts);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * If loading failed, then make sure to error out regardless. It's not
+ * really an error to not have a hosts file in non-strict modes but if
+ * there is one and it fails to load properly, then silently pressing
+ * on seems worse than raising an error.
+ */
+ if (!res)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"));
+ return -1;
+ }
+
+ /*
+ * In strict ssl_snimode there needs to be at least one configured
+ * host in the pg_hosts file since the default fallback context isn't
+ * allowed to connect with.
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load %s", "pg_hosts.conf"),
+ errhint("In strict ssl_snimode there needs to be at least one entry in pg_hosts.conf."));
+ return -1;
+ }
+
+ /*
+ * Loading and parsing the hosts file was successful, create contexts
+ * for each host entry and add to the the list of host to be checked
+ * during login.
+ */
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ static SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ MemoryContextDelete(hostcxt);
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ /*
+ * The parsing logic has already verified that the hostname exist
+ * so we need not check that.
+ */
+ host_context = palloc0(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = tmp_context;
+ host_context->default_host = false;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into this
+ * SSL_context.
+ */
+ if (host->ssl_ca && host->ssl_ca[0] != '\0')
+ host_context->ssl_loaded_verify_locations = true;
+
+ contexts = lappend(contexts, host_context);
+ }
+
+ MemoryContextDelete(hostcxt);
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (list_length(contexts) < 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -132,10 +330,17 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in case the server is configured to
+ * validate hostnames.
+ */
+ if (ssl_snimode != SSL_SNIMODE_OFF)
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -143,16 +348,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -161,19 +366,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -325,17 +530,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -348,16 +553,20 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
+ }
+
+ /*
+ * If we have a CA store, or SNI is enabled, always ask for SSL client
+ * cert, but don't fail if it's not presented. We might fail such
+ * connections later, depending on what we find in pg_hba.conf. The reason
+ * for enabling in the case of SNI even if there is no CA is that another
+ * context might have a CA, so the callback must be installed in order for
+ * that context.
+ */
+ if (ctx_ssl_ca_file[0] || ssl_snimode != SSL_SNIMODE_OFF)
SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
verify_cb);
- }
/*----------
* Load the Certificate Revocation List (CRL).
@@ -407,38 +616,19 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
- SSL_context = NULL;
- ssl_loaded_verify_locations = false;
+ free_contexts();
}
int
@@ -771,6 +961,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1144,7 +1337,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1390,6 +1583,131 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Depending on the SNI mode we either
+ * require a perfect match, or we allow to fallback to a default configuration.
+ * Returning SSL_TLSEXT_ERR_ALERT_FATAL to OpenSSL will immediately terminate
+ * the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+
+ /*
+ * Executing this callback when SNI is turned off indicates a programmer
+ * error or something worse. Throw an assertion to catch during testing
+ * but also ensure to terminate the connection in non-assert builds, even
+ * though this should never happen, just to be on the safe side.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF)
+ {
+ Assert(false);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options.
+ * For ssl_snimode strict we error out since we cannot match a host config
+ * for the connection. For the default mode we fall back on the default
+ * hostname configuration.
+ */
+ if (!tlsext_hostname)
+ {
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ else
+ {
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * We have a requested hostname from the client, match against all entries
+ * in the pg_hosts configuration to find a match.
+ */
+ foreach_ptr(HostContext, host, contexts)
+ {
+ /*
+ * For strict mode we will never want the default host so we can skip
+ * past it immediately.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host)
+ continue;
+
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host;
+ SSL_context = host->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch SSL context for SNI host"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ /*
+ * At this point we know that the requested hostname isn't configured in
+ * the pg_hosts file. In ssl_snimode "strict" it's an error if there was
+ * no match for the hostname in the TLS extension so terminate the
+ * connection.
+ */
+ if (ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no matching pg_hosts entry found for hostname: \"%s\"",
+ tlsext_hostname)));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ /*
+ * In ssl_snimode "default" we fall back on the default host configured in
+ * postgresql.conf when no match is found in pg_hosts.conf.
+ */
+ Host_context = Default_context;
+ SSL_context = Host_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to default SSL context"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1599,6 +1917,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1792,17 +2116,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1814,3 +2144,24 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we Must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (contexts == NIL)
+ return;
+
+ foreach_ptr(HostContext, host, contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(contexts);
+ contexts = NIL;
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..f6c1422b555 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -60,6 +56,8 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+int ssl_snimode = SSL_SNIMODE_OFF;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
@@ -99,7 +97,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..5a47f9cae7d
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c6484aea087..f35a15b48df 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..30cde4ee800 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1167,6 +1167,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
@@ -2742,6 +2749,14 @@
max => '0',
},
+{ name => 'ssl_snimode', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets the SNI mode to use for the server.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_snimode',
+ boot_val => 'SSL_SNIMODE_OFF',
+ options => 'ssl_snimode_options',
+},
+
{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.',
long_desc => 'An empty string means use the default cipher suites.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..cd2074d54e1 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry ssl_snimode_options[] = {
+ {"off", SSL_SNIMODE_OFF, false},
+ {"default", SSL_SNIMODE_DEFAULT, false},
+ {"strict", SSL_SNIMODE_STRICT, false},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -556,6 +563,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..c5ff8302201 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = off
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..c953f24a58d 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2829,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e3748d3c8c9..c96818549cc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,25 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 5af005ad779..54d8ee8a2aa 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir;
extern PGDLLIMPORT char *ssl_key_file;
extern PGDLLIMPORT int ssl_min_protocol_version;
extern PGDLLIMPORT int ssl_max_protocol_version;
+extern PGDLLIMPORT int ssl_snimode;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
@@ -152,12 +153,20 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+enum ssl_snimode
+{
+ SSL_SNIMODE_OFF = 0,
+ SSL_SNIMODE_DEFAULT,
+ SSL_SNIMODE_STRICT
+};
+
/*
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern bool load_hosts(List **hosts);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da89..8f08a38b789 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..55e0f04d4f5 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches against the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..90d7560ad11
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,237 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostname used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1";
+
+$node->append_conf('postgresql.conf', "ssl_snimode=default");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+# example.org serves the server cert and its intermediate CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect with configured hostname, serving intermediate server CA");
+
+$node->connect_fails(
+ "$connstr sslrootcert=invalid sslmode=verify-ca",
+ "connect without server root cert sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "connect still fails with fallback hostname, without intermediate",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca",
+ "connect with fallback hostname, intermediate included");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "localhost server-cn-only.crt server-cn-only.key root_ca.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with missing hostconfig and snimode=strict",
+ expected_stderr => qr/tlsv1 unrecognized name/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1",
+ "connect with correct server CA cert file sslmode=require");
+
+# Attempts at connecting without SNI when the server is using strict mode should
+# result in connection failure.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "connect with correct server CA cert file without SNI for strict mode",
+ expected_stderr => qr/tlsv13 alert missing extension/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file after more reloads");
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect fails since the passphrase protected key cannot be reloaded");
+
+# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
+# time swapping out default contexts containing different CA configurations.
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+foreach my $default_ca ("", "root+client_ca", "root+server_ca")
+{
+ # The default CA should, not matter for the purposes of these tests, since
+ # we connect to the other hosts explicitly. Test with various default CA
+ # settings to ensure it's isolated from the actual connections.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
+ expected_stderr => qr/certificate verify failed/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
+ );
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
+ expected_stderr => qr/certificate verify failed/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 4159addb700..bbd3bed6c86 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index dfcd619bfee..9c580db6b7d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1208,6 +1208,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-03 09:57 Heikki Linnakangas <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Heikki Linnakangas @ 2025-12-03 09:57 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Dewei Dai <[email protected]>; +Cc: li.evan.chao <[email protected]>; Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Sorry for jumping in so late.
On Fri, May 10, 2024 at 7:23 AM Daniel Gustafsson
<daniel(at)yesql(dot)se> wrote:
> The attached patch adds serverside SNI support to libpq, it is still a bit
> rough around the edges but I'm sharing it early to make sure I'm not designing
> it in a direction that the community doesn't like. A new config file
> $datadir/pg_hosts.conf is used for configuring which certicate and key should
> be used for which hostname. The file is parsed in the same way as pg_ident
> et.al so it allows for the usual include type statements we support. A new
> GUC, ssl_snimode, is added which controls how the hostname TLS extension is
> handled. The possible values are off, default and strict:
>
>
> - off: pg_hosts.conf is not parsed and the hostname TLS extension is
> not inspected at all. The normal SSL GUCs for certificates and keys
> are used.
> - default: pg_hosts.conf is loaded as well as the normal GUCs. If no
> match for the TLS extension hostname is found in pg_hosts the cert
> and key from the postgresql.conf GUCs is used as the default (used
> as a wildcard host).
> - strict: only pg_hosts.conf is loaded and the TLS extension hostname
> MUST be passed and MUST have a match in the configuration, else the
> connection is refused.
>
>
> As of now the patch use default as the initial value for the GUC
Do we need the GUC? It feels a little confusing that a GUC affects how
the settings in the pg_hosts.conf are interepreted. It'd be nice if you
could open pg_hosts.conf in an editor, and see at one glance everything
that affects this.
I propose that there is no GUC. In 'pg_hosts.conf', you can specify a
wildcard '*' host that matches anything. You can also specify a "no sni"
line which matches connections with no SNI specified. (Or something
along those lines, I didn't think too hard about all the interactions).
Should we support wildcards like "*.example.com* too?
For backwards-compatibility, if you specify a certificate and key in
postgresql.conf, they are treated the same as if you had a "*" line in
pg_hosts.conf.
- Heikki
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-03 16:52 Daniel Gustafsson <[email protected]>
parent: Heikki Linnakangas <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-12-03 16:52 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 3 Dec 2025, at 10:57, Heikki Linnakangas <[email protected]> wrote:
>
> Sorry for jumping in so late.
Not at all, thanks for looking!
> Do we need the GUC? It feels a little confusing that a GUC affects how the settings in the pg_hosts.conf are interepreted. It'd be nice if you could open pg_hosts.conf in an editor, and see at one glance everything that affects this.
I added the GUC for two reasons; as a way to opt-out of this feature if it's
something that the admin doesn't want; and as a way to set the SNI mode. There
are currently the two modes of STRICT and DEFAULT which affects how incoming
connections are handled. The first motivation might be unfounded, and the
second one could be encoded in a pg_hosts configuration though implicitly
rather than explicitly.
Having all the details in pg_hosts.conf is appealing, no disagreement there,
but it does pose some challenges in the interaction with the postgresql.conf
GUCS (more later).
> I propose that there is no GUC. In 'pg_hosts.conf', you can specify a wildcard '*' host that matches anything. You can also specify a "no sni" line which matches connections with no SNI specified. (Or something along those lines, I didn't think too hard about all the interactions).
So basically reserving a hostname,"no_sni" or something, which indicates that
it's for non sslsni connections? That should work, with the parsing rule that
there can only be one in the file.
> Should we support wildcards like "*.example.com* too?
I have that on my if-it-gets-committed TODO but I kept it out of the initial
proposal to keep complexity down and goalposts in sight.
> For backwards-compatibility, if you specify a certificate and key in postgresql.conf, they are treated the same as if you had a "*" line in pg_hosts.conf.
That's a bit trickier though, since the cert/key have a default boot_val so
they will always be set to something unless the user enables ssl=on and at the
same time uncomments ssl_cert_file/ssl_key_file and set them to '' before
proceeding to add configuration in pg_hosts.conf. This is pretty unintuitive I
think. unintuitive. This backwards comatibility is one of the reasons I kept
the postgresl.conf values for the default context config.
I really want to make it possible for anyone who don't want SNI to keep using
postgresql.conf and get the exact behavior they've always had. Do you agree
with that design goal?
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-03 16:56 Heikki Linnakangas <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Heikki Linnakangas @ 2025-12-03 16:56 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On 03/12/2025 18:52, Daniel Gustafsson wrote:
>> On 3 Dec 2025, at 10:57, Heikki Linnakangas <[email protected]> wrote:
>> Do we need the GUC? It feels a little confusing that a GUC affects how the settings in the pg_hosts.conf are interepreted. It'd be nice if you could open pg_hosts.conf in an editor, and see at one glance everything that affects this.
>
> I added the GUC for two reasons; as a way to opt-out of this feature if it's
> something that the admin doesn't want; and as a way to set the SNI mode. There
> are currently the two modes of STRICT and DEFAULT which affects how incoming
> connections are handled. The first motivation might be unfounded, and the
> second one could be encoded in a pg_hosts configuration though implicitly
> rather than explicitly.
>
> Having all the details in pg_hosts.conf is appealing, no disagreement there,
> but it does pose some challenges in the interaction with the postgresql.conf
> GUCS (more later).
>
>> I propose that there is no GUC. In 'pg_hosts.conf', you can specify a wildcard '*' host that matches anything. You can also specify a "no sni" line which matches connections with no SNI specified. (Or something along those lines, I didn't think too hard about all the interactions).
>
> So basically reserving a hostname,"no_sni" or something, which indicates that
> it's for non sslsni connections? That should work, with the parsing rule that
> there can only be one in the file.
Yeah, something like that. And to implement the "strict" mode, you could
have a "no_sni" line with no cert/key specified.
>> For backwards-compatibility, if you specify a certificate and key in postgresql.conf, they are treated the same as if you had a "*" line in pg_hosts.conf.
>
> That's a bit trickier though, since the cert/key have a default boot_val so
> they will always be set to something unless the user enables ssl=on and at the
> same time uncomments ssl_cert_file/ssl_key_file and set them to '' before
> proceeding to add configuration in pg_hosts.conf. This is pretty unintuitive I
> think. unintuitive. This backwards comatibility is one of the reasons I kept
> the postgresl.conf values for the default context config.
>
> I really want to make it possible for anyone who don't want SNI to keep using
> postgresql.conf and get the exact behavior they've always had. Do you agree
> with that design goal?
Yeah, that's fair.
- Heikki
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-03 21:27 Jelte Fennema-Nio <[email protected]>
parent: Heikki Linnakangas <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Jelte Fennema-Nio @ 2025-12-03 21:27 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, 3 Dec 2025 at 17:57, Heikki Linnakangas <[email protected]> wrote:
> > I really want to make it possible for anyone who don't want SNI to keep using
> > postgresql.conf and get the exact behavior they've always had. Do you agree
> > with that design goal?
>
> Yeah, that's fair.
What if we make it so that if a pg_hosts.conf file exists, then the
ssl_cert_file/ssl_key_file configs are ignored? And by default initdb
would not create a file (or it would, but with the same default
settings that we have now). Then we don't need the new GUC. Basically
it would be:
1. If the file does not exist, use the "off" behaviour
2. If the file exists, use the "strict" behaviour
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-03 23:27 Daniel Gustafsson <[email protected]>
parent: Jelte Fennema-Nio <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-12-03 23:27 UTC (permalink / raw)
To: Jelte Fennema-Nio <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Jacob Champion <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 3 Dec 2025, at 22:27, Jelte Fennema-Nio <[email protected]> wrote:
>
> On Wed, 3 Dec 2025 at 17:57, Heikki Linnakangas <[email protected]> wrote:
>>> I really want to make it possible for anyone who don't want SNI to keep using
>>> postgresql.conf and get the exact behavior they've always had. Do you agree
>>> with that design goal?
>>
>> Yeah, that's fair.
>
> What if we make it so that if a pg_hosts.conf file exists, then the
> ssl_cert_file/ssl_key_file configs are ignored? And by default initdb
> would not create a file (or it would, but with the same default
> settings that we have now).
Maybe. I'm not a big fan of magic-file-exist configurations but.. I'm trying
out a few different options to see which seems the most reasonable, and this is
for one of them.
> Basically it would be:
> 1. If the file does not exist, use the "off" behaviour
> 2. If the file exists, use the "strict" behaviour
It will really be "strict" *or* "default" based on whether or not '*' is set as
a wildcard hostname (which can be argued is just a version of strict).
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-11 17:47 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Jacob Champion @ 2025-12-11 17:47 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Hi!
On Mon, Nov 24, 2025 at 6:53 AM Daniel Gustafsson <[email protected]> wrote:
> The attached incorporates your tests, fixes them to make them pass. The
> culprit seemed to be a combination of a bug in the code (the verify callback
> need to be defined in the default context even if there is no CA for it to be
> called in an SNI setting because OpenSSL), and that the tests were matching
> backend errors against frontend messages.
The new v12 tests still don't pass for me (they all use "certificate
verify failed", but the failure modes should be different).
> + if (host->ssl_ca && host->ssl_ca[0] != '\0')
The comment for HostsLine.ssl_ca, and the code that assigns it,
implies to me that host->ssl_ca should never be NULL. Am I missing a
case where it could be?
On Wed, Dec 3, 2025 at 1:57 AM Heikki Linnakangas <[email protected]> wrote:
> I propose that there is no GUC. In 'pg_hosts.conf', you can specify a
> wildcard '*' host that matches anything. You can also specify a "no sni"
> line which matches connections with no SNI specified. (Or something
> along those lines, I didn't think too hard about all the interactions).
That seems to position SNI as a feature that every DBA should have to
think about by default. ("learn this file. you can't turn it off.") Is
it, yet?
Web servers enable SNI implicitly because name-based hosting is a
top-level concept for users over there (hostnames are baked into the
application layer). I would argue that we don't have that here. Maybe
in the future someone will ask for that, but at that point don't you
want a very different, name-based, config system?
On Wed, Dec 3, 2025 at 3:28 PM Daniel Gustafsson <[email protected]> wrote:
> > On 3 Dec 2025, at 22:27, Jelte Fennema-Nio <[email protected]> wrote:
> > What if we make it so that if a pg_hosts.conf file exists, then the
> > ssl_cert_file/ssl_key_file configs are ignored? And by default initdb
> > would not create a file (or it would, but with the same default
> > settings that we have now).
>
> Maybe. I'm not a big fan of magic-file-exist configurations
Me neither. (I especially don't like the idea of ignoring a
certificate+key setting that a user has taken the time to put into a
config.)
Thanks,
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-11 17:51 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-12-11 17:51 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 11 Dec 2025, at 18:47, Jacob Champion <[email protected]> wrote:
> The new v12 tests still don't pass for me (they all use "certificate
> verify failed", but the failure modes should be different).
In which version of OpenSSL (or LibreSSL)?
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-11 19:40 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Jacob Champion @ 2025-12-11 19:40 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Thu, Dec 11, 2025 at 9:52 AM Daniel Gustafsson <[email protected]> wrote:
> > The new v12 tests still don't pass for me (they all use "certificate
> > verify failed", but the failure modes should be different).
>
> In which version of OpenSSL (or LibreSSL)?
1.1.1 through 3.6. The CI for this commitfest entry shows it too:
https://cirrus-ci.com/task/5648027525840896
Local diff that missed `git add`, maybe?
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-12 11:41 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 2 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2025-12-12 11:41 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 11 Dec 2025, at 18:47, Jacob Champion <[email protected]> wrote:
> On Mon, Nov 24, 2025 at 6:53 AM Daniel Gustafsson <[email protected]> wrote:
>> The attached incorporates your tests, fixes them to make them pass. The
>> culprit seemed to be a combination of a bug in the code (the verify callback
>> need to be defined in the default context even if there is no CA for it to be
>> called in an SNI setting because OpenSSL), and that the tests were matching
>> backend errors against frontend messages.
>
> The new v12 tests still don't pass for me (they all use "certificate
> verify failed", but the failure modes should be different).
I'm still not sure why they pass for me locally with that error, but I've
updated to patch to match CI.
>> + if (host->ssl_ca && host->ssl_ca[0] != '\0')
>
> The comment for HostsLine.ssl_ca, and the code that assigns it,
> implies to me that host->ssl_ca should never be NULL. Am I missing a
> case where it could be?
The attached version allows ssl_ca to be omitted from the pg_host config to
match the ssl_ca GUC.
> On Wed, Dec 3, 2025 at 1:57 AM Heikki Linnakangas <[email protected]> wrote:
>> I propose that there is no GUC. In 'pg_hosts.conf', you can specify a
>> wildcard '*' host that matches anything. You can also specify a "no sni"
>> line which matches connections with no SNI specified. (Or something
>> along those lines, I didn't think too hard about all the interactions).
The attached version removes the GUC and instead sets a precedence rule that if
pg_hosts exists and is non-empty, it is exclusively used. If it doesn't exist,
or is empty, then the regular SSL GUCs are used.
Further, pg_hosts is extended with handling * for default fallback, and no_sni
for rules targeting connections with no hostname. The docs changes were harder
than implementing the code, suggestions on how to improve that part would be
greatly appreciated.
But, see below.
>> Maybe. I'm not a big fan of magic-file-exist configurations
>
> Me neither. (I especially don't like the idea of ignoring a
> certificate+key setting that a user has taken the time to put into a
> config.)
I wonder if the way forward is to do both? Heikki has a good point that when
working with pg_hosts.conf it should be clear from just that file what the
final config will be, and in the previous version that wasn't the case since
the ssl_snimode GUC set operation modes. At the same time, Jacob has a point
that overriding configuration just because pg_hosts exists isn't transparent.
Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can perhaps
fix both complaints? If the GUC is on, pg_hosts - and only pg_hosts - is used
for configuring secrets. By using the * fallback and no_sni rule in pg_hosts
all variations of configs can be achieved. If the GUC is off, then the regular
SSL GUCs are used and pg_host is never considered (and thus SNI is not
possible).
Such a GUC wouldn't make the patch all that much different from what it is
right now. What do you think about that middleground proposal?
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v13-0001-Serverside-SNI-support-for-libpq.patch (59.9K, 2-v13-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 915e41e110dad5dd9c99e7bbcb629a829d3474b0 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 11 Dec 2025 18:38:00 +0100
Subject: [PATCH v13] Serverside SNI support for libpq
Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certificate/key handling
is available per host. A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname. If pg_hosts.conf is non-empty it will take precedence over
the regular SSL GUCs, if it is empty or missing the regular GUCs will
be used just as before this commit with no hostname specific handling.
Host configuration can either be for a literal hostname to match, non-
SNI connections using the no_sni keyword or a default fallback matching
all connections. By omitting no_sni and the fallback a strict mode
can be achieved where only connections using sslsni=1 and a specified
hostname are allowed.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Dewei Dai <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/runtime.sgml | 118 +++++
src/backend/Makefile | 2 +
src/backend/libpq/be-secure-common.c | 198 ++++++++-
src/backend/libpq/be-secure-openssl.c | 415 ++++++++++++++++--
src/backend/libpq/be-secure.c | 6 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 7 +
src/backend/utils/misc/guc_tables.c | 1 +
src/backend/utils/misc/postgresql.conf.sample | 2 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 27 ++
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 3 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/001_ssltests.pl | 6 +-
src/test/ssl/t/004_sni.pl | 289 ++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 +-
src/tools/pgindent/typedefs.list | 3 +
23 files changed, 1120 insertions(+), 68 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..ca0a114da76 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,118 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for Server Name
+ Indication, <acronym>SNI</acronym>, using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection based on
+ the hosts which are defined in <filename>pg_hosts.conf</filename>.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the cluster's
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+
+ <para>
+ <replaceable>hostname</replaceable> should either be set to the literal
+ hostname for the connection, <literal>no_sni</literal> or <literal>*</literal>.
+ <xref linkend="hostname-values"/> contain details on how these values are
+ used.
+ <table id="hostname-values">
+ <title>Hostname setting values</title>
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Host Entry</entry>
+ <entry>sslsni</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal>*</literal></entry>
+ <entry>Not required</entry>
+ <entry>Default host, matches all connections</entry>
+ </row>
+
+ <row>
+ <entry><literal>no_sni</literal></entry>
+ <entry>Not allowed</entry>
+ <entry>
+ Certificate and key to use for connection with no <literal>sslsni</literal> defined.
+ </entry>
+ </row>
+
+ <row>
+ <entry><replaceable>hostname</replaceable></entry>
+ <entry>Required</entry>
+ <entry>
+ Certificate and key to use for connections to the host specified in the
+ connection.
+ </entry>
+ </row>
+ </tbody>
+
+ </tgroup>
+ </table>
+ </para>
+
+ <para>
+ If <filename>pg_hosts.conf</filename> is empty, or missing, then the SSL
+ configuration in <filename>postgresql.conf</filename> will be used for all
+ connections. If <filename>pg_hosts.conf</filename> is non-empty then it
+ will take precedence over certificate and key settings in
+ <filename>postgresql.conf</filename>.
+ </para>
+
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+
+ <para>
+ The CRL configuration in <filename>postgresql.conf</filename> is applied
+ on all connections regardless of if they use SNI or not.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..529126eebeb 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -187,6 +187,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
@@ -246,6 +247,7 @@ endif
$(MAKE) -C utils uninstall-data
rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \
'$(DESTDIR)$(datadir)/pg_ident.conf.sample' \
+ '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \
'$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
$(call uninstall_llvm_module,postgres)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index e8b837d1fa7..be703a87636 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ const char *cmd = (const char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,187 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (optional) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ return parsedline;
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file. The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false. This function is
+ * intended to be executed within a temporary memory context which can be
+ * discarded to free memory allocated during the processing of the file.
+ */
+int
+load_hosts(List **hosts, char **err_msg)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ /*
+ * If we cannot return results then error out immediately. This implies
+ * API misuse or a similar kind of programmer error.
+ */
+ if (!hosts)
+ return HOSTSFILE_LOAD_FAILED;
+ *hosts = NIL;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, err_msg);
+ if (file == NULL)
+ {
+ if (errno == ENOENT)
+ return HOSTSFILE_MISSING;
+
+ return HOSTSFILE_LOAD_FAILED;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if ((tok_line->err_msg != NULL) ||
+ ((newline = parse_hosts_line(tok_line, LOG)) == NULL))
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ *hosts = parsed_lines;
+
+ if (!ok)
+ return HOSTSFILE_LOAD_FAILED;
+
+ if (parsed_lines == NIL)
+ return HOSTSFILE_EMPTY;
+
+ return HOSTSFILE_LOAD_OK;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 37f4d97f209..004a93e4a89 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,15 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ SSL_CTX *context;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +79,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +87,26 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+/* List of SSL contexts for hostname defined connections */
+static List *sni_contexts = NIL;
+
+/* The default SSL context to use as fallback in case no hostname matches */
+static HostContext *default_context = NULL;
+
+/* The SSL context to use for connections without SNI */
+static HostContext *no_sni_context = NULL;
+
+/* The currently active context */
static SSL_CTX *SSL_context = NULL;
+static HostContext *Host_context = NULL;
+
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
struct CallbackErr
{
@@ -102,11 +123,181 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
+{
+ List *pg_hosts = NIL;
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext host_memcxt;
+ char *err_msg;
+ int res;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (sni_contexts != NIL || default_context || no_sni_context)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ }
+
+ /*
+ * Attempt to load, and parse, TLS configuration from the pg_hosts.conf
+ * file with the set of hosts returned as a list. If there are hosts
+ * configured there they take precedence over the postgresql.conf config.
+ * Make sure to allocate the parsed rows in a temporary memory context so
+ * that we can avoid memory leaks from the parsing process.
+ */
+ host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(host_memcxt);
+ res = load_hosts(&pg_hosts, &err_msg);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * pg_hosts.conf is not required to contain configuration, but if it does
+ * we error out in case it fails to load rather than continue to try the
+ * postgresql.conf configuration to avoid silently falling back on an
+ * undesired configuration.
+ */
+ if (res == HOSTSFILE_LOAD_FAILED)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load \"%s\": %s", "pg_hosts.conf",
+ err_msg ? err_msg : "unknown error"));
+ MemoryContextDelete(host_memcxt);
+ return -1;
+ }
+
+ /*
+ * Loading and parsing the hosts file was successful, create contexts for
+ * each host entry and add to the list of hosts to be checked during
+ * login.
+ */
+ else if (res == HOSTSFILE_LOAD_OK)
+ {
+ foreach(line, pg_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load SSL config from \"%s\" line %i",
+ host->sourcefile, host->linenumber));
+ free_contexts();
+ MemoryContextDelete(host_memcxt);
+ return -1;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+ host_context->context = tmp_context;
+
+ /* Set flag to remember whether CA store has been loaded */
+ if (host->ssl_ca && host->ssl_ca[0] != '\0')
+ host_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * The hostname in the context is NULL in case it is the default
+ * host, or a context to use for non-SNI connections.
+ */
+ if (strcmp(host->hostname, "*") == 0)
+ default_context = host_context;
+ else if (strcmp(host->hostname, "no_sni") == 0)
+ no_sni_context = host_context;
+ else
+ {
+ host_context->hostname = pstrdup(host->hostname);
+ sni_contexts = lappend(sni_contexts, host_context);
+ }
+
+ /*
+ * There needs to be an installed context to drive the handshake
+ * until the SNI callback switches over to the expected one, for
+ * now just set it to the first one we see.
+ */
+ if (!Host_context)
+ Host_context = host_context;
+ }
+
+ MemoryContextDelete(host_memcxt);
+ }
+
+ /*
+ * If the pg_hosts.conf file doesn't exist, or is empty, then load the
+ * config from postgresql.conf.
+ */
+ else if (res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
+ {
+ HostsLine pgconf;
+ SSL_CTX *tmp_context = NULL;
+
+ memset(&pgconf, 0, sizeof(pgconf));
+ pgconf.ssl_cert = ssl_cert_file;
+ pgconf.ssl_key = ssl_key_file;
+ pgconf.ssl_ca = ssl_ca_file;
+ pgconf.ssl_passphrase_cmd = ssl_passphrase_command;
+ pgconf.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ tmp_context = ssl_init_context(isServerStart, &pgconf);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load SSL configuration from \"%s\"",
+ "postgresql.conf"));
+ return -1;
+ }
+
+ /*
+ * If postgresql.conf is used to configure SSL then by definition it
+ * will be the default context as we don't have per-host config. We
+ * can also set it as the Host_context since it will be used for all
+ * connections.
+ */
+ default_context = palloc0(sizeof(HostContext));
+ default_context->context = tmp_context;
+ Host_context = default_context;
+
+ /* Set flag to remember whether CA store has been loaded */
+ if (ssl_ca_file[0])
+ default_context->ssl_loaded_verify_locations = true;
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (sni_contexts == NIL && !default_context && !no_sni_context)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ SSL_context = Host_context->context;
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -132,10 +323,16 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in order to validate hostnames in
+ * case we have at least one context configured with a host name.
+ */
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -143,16 +340,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -161,19 +358,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -325,17 +522,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file && ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -347,18 +544,17 @@ be_tls_init(bool isServerStart)
* free it when no longer needed.
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
-
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
- SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
- verify_cb);
}
+ /*
+ * Always ask for SSL client cert, but don't fail if it's not presented.
+ * We might fail such connections later, depending on what we find in
+ * pg_hba.conf.
+ */
+ SSL_CTX_set_verify(context,
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+ verify_cb);
+
/*----------
* Load the Certificate Revocation List (CRL).
* http://searchsecurity.techtarget.com/sDefinition/0,,sid14_gci803160,00.html
@@ -407,38 +603,19 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
- SSL_context = NULL;
- ssl_loaded_verify_locations = false;
+ free_contexts();
}
int
@@ -771,6 +948,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1144,7 +1324,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1390,6 +1570,92 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Returning SSL_TLSEXT_ERR_ALERT_FATAL to
+ * OpenSSL will immediately terminate the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ HostContext *install_context = NULL;
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options:
+ * i) there is a HostContext defined for non-SNI connections, in that case
+ * we switch to that; ii) there is no non-SNI config and we error out as
+ * there is no context to switch to.
+ */
+ if (!tlsext_hostname)
+ {
+ if (no_sni_context)
+ install_context = no_sni_context;
+ else if (default_context)
+ install_context = default_context;
+ else
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ }
+ else
+ {
+ /*
+ * We have a requested hostname from the client, match against all
+ * entries in the pg_hosts configuration and attempt to find a match.
+ */
+ foreach_ptr(HostContext, host, sni_contexts)
+ {
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ install_context = host;
+ break;
+ }
+ }
+
+ /*
+ * If no host specific match was found, and there is a default config,
+ * then fall back to using that.
+ */
+ if (!install_context && default_context)
+ install_context = default_context;
+ }
+
+ /*
+ * If we reach here without a context chosen as the session context then
+ * fail the handshake and terminate the connection.
+ */
+ if (install_context == NULL)
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+
+ Host_context = install_context;
+ SSL_context = install_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to SSL context for host"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1599,6 +1865,14 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ if (!Host_context)
+ return false;
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1792,17 +2066,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1814,3 +2094,42 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (sni_contexts != NIL)
+ {
+ foreach_ptr(HostContext, host, sni_contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(sni_contexts);
+ sni_contexts = NIL;
+ }
+
+ /*
+ * The hostname need not be freed for the no_sni and default contexts
+ * since they by definition are not connected to a hostname and thus have
+ * none allocated.
+ */
+ if (no_sni_context)
+ {
+ SSL_CTX_free(no_sni_context->context);
+ pfree(no_sni_context);
+ no_sni_context = NULL;
+ }
+ if (default_context)
+ {
+ SSL_CTX_free(default_context->context);
+ pfree(default_context);
+ default_context = NULL;
+ }
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index d723e74e813..2e6be47887c 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -99,7 +95,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 31aa2faae1e..4f6ec13bc74 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..a31c49b01f7
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY SSL CA PASSPHRASE COMMAND PASSPHRASE COMMAND RELOAD
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 935c235e1b3..2e30e564715 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..4ce670e8347 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1167,6 +1167,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..1d26628f879 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -556,6 +556,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..1f360110564 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..c953f24a58d 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1530,6 +1531,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2791,6 +2800,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2806,12 +2816,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2819,6 +2829,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7b93ba4a709..38713381255 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,33 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
+typedef enum HostsFileLoad
+{
+ HOSTSFILE_LOAD_OK = 0,
+ HOSTSFILE_LOAD_FAILED,
+ HOSTSFILE_EMPTY,
+ HOSTSFILE_MISSING,
+} HostsFileLoadResult;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d6e671a6382..e1631cb7b5c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port);
extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern bool be_tls_loaded_verify_locations(void);
/*
* Get the server certificate hash for SCRAM channel binding type
@@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
/* init hook for SSL, the default sets the password callback if appropriate */
#ifdef USE_OPENSSL
-typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart);
+typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host);
extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook;
#endif
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 5af005ad779..b9b2c8bd5af 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -156,8 +156,9 @@ enum ssl_protocol_versions
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern int load_hosts(List **hosts, char **err_msg);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da89..8f08a38b789 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 295988b8b87..11f9280b341 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches against the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index d8e0fb518e0..e5a9402cd9c 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index fc7c35ef879..15ca0a0e8c2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -380,11 +380,11 @@ switch_server_cert($node, certfile => 'server-ip-cn-only');
$common_connstr =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in the Common Name");
$node->connect_fails(
- "$common_connstr host=192.000.002.001",
+ "$common_connstr host=192.000.002.001 sslsni=0",
"mismatch between host name and server certificate IP address",
expected_stderr =>
qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
@@ -394,7 +394,7 @@ $node->connect_fails(
# long-standing behavior.)
switch_server_cert($node, certfile => 'server-ip-in-dnsname');
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in a dNSName");
# Test Subject Alternative Names.
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..2dd70e7afee
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,289 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostaddr used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR sslsni=1";
+
+##############################################################################
+# postgresql.conf
+##############################################################################
+
+# Connect without any hosts configured in pg_hosts.conf, thus using the cert
+# and key in postgresql.conf. pg_hosts.conf exists at this point but is empty
+# apart from the comments stemming from the sample.
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg.conf: connect fails without intermediate for sslmode=verify-ca",
+ expected_stderr => qr/certificate verify failed/);
+
+# Remove pg_hosts.conf and reload to make sure a missing file is treated like
+# an empty file.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect after deleting pg_hosts.conf");
+
+##############################################################################
+# pg_hosts.conf
+##############################################################################
+
+# Replicate the postgresql.conf configuration into pg_hosts.conf and retry the
+# same tests as above.
+$node->append_conf('pg_hosts.conf',
+ "* server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg_hosts.conf: connect to default, with correct server CA cert file sslmode=require"
+);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to default, fail without intermediate for sslmode=verify-ca",
+ expected_stderr => qr/certificate verify failed/);
+
+# Add host entry for example.org which serves the server cert and its
+# intermediate CA. The previously existing default host still exists without
+# a CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org and verify server CA");
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=invalid sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org but without server root cert, sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to default and fail to verify CA",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg_hosts.conf: connect to default with sslmode=require");
+
+# Modify pg_hosts.conf to no longer have the default host entry.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+# Connecting without a hostname as well as with a hostname which isn't in the
+# pg_hosts configuration should fail.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/missing extension/);
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.com",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+);
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file after more reloads"
+);
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+ );
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect fails since the passphrase protected key cannot be reloaded"
+);
+
+# Configure with only non-SNI connections allowed
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "no_sni server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "pg_hosts.conf: only non-SNI connections allowed");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.org",
+ "pg_hosts.conf: only non-SNI connections allowed, connecting with SNI",
+ expected_stderr => qr/unrecognized name/);
+
+# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
+# time swapping out default contexts containing different CA configurations.
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+foreach my $default_ca ("", "root+client_ca", "root+server_ca")
+{
+ # The default CA should, not matter for the purposes of these tests, since
+ # we connect to the other hosts explicitly. Test with various default CA
+ # settings to ensure it's isolated from the actual connections.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
+ expected_stderr => qr/unknown ca/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/
+ );
+
+ $node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
+ );
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/
+ );
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
+ expected_stderr => qr/unknown ca/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 4159addb700..bbd3bed6c86 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9dd65b10254..43bd1ac3f99 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1209,6 +1209,9 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsFileLoadResult
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-17 09:03 Heikki Linnakangas <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Heikki Linnakangas @ 2025-12-17 09:03 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On 12/12/2025 13:41, Daniel Gustafsson wrote:
>> On Wed, Dec 3, 2025 at 1:57 AM Heikki Linnakangas <[email protected]> wrote:
>>> Maybe. I'm not a big fan of magic-file-exist configurations
>>
>> Me neither. (I especially don't like the idea of ignoring a
>> certificate+key setting that a user has taken the time to put into a
>> config.)
+1
> I wonder if the way forward is to do both? Heikki has a good point that when
> working with pg_hosts.conf it should be clear from just that file what the
> final config will be, and in the previous version that wasn't the case since
> the ssl_snimode GUC set operation modes. At the same time, Jacob has a point
> that overriding configuration just because pg_hosts exists isn't transparent.
>
> Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can perhaps
> fix both complaints? If the GUC is on, pg_hosts - and only pg_hosts - is used
> for configuring secrets. By using the * fallback and no_sni rule in pg_hosts
> all variations of configs can be achieved. If the GUC is off, then the regular
> SSL GUCs are used and pg_host is never considered (and thus SNI is not
> possible).
>
> Such a GUC wouldn't make the patch all that much different from what it is
> right now. What do you think about that middleground proposal?
I like that.
Instead of a boolean GUC, it could perhaps be a path to the pg_hosts
file. I haven't thought this through but somehow it feels more natural
to me than a "read this file or not" setting.
- Heikki
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-17 09:06 Heikki Linnakangas <[email protected]>
parent: Heikki Linnakangas <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Heikki Linnakangas @ 2025-12-17 09:06 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On 17/12/2025 11:03, Heikki Linnakangas wrote:
> On 12/12/2025 13:41, Daniel Gustafsson wrote:
>> I wonder if the way forward is to do both? Heikki has a good point
>> that when
>> working with pg_hosts.conf it should be clear from just that file what
>> the
>> final config will be, and in the previous version that wasn't the case
>> since
>> the ssl_snimode GUC set operation modes. At the same time, Jacob has
>> a point
>> that overriding configuration just because pg_hosts exists isn't
>> transparent.
>>
>> Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can
>> perhaps
>> fix both complaints? If the GUC is on, pg_hosts - and only pg_hosts -
>> is used
>> for configuring secrets. By using the * fallback and no_sni rule in
>> pg_hosts
>> all variations of configs can be achieved. If the GUC is off, then
>> the regular
>> SSL GUCs are used and pg_host is never considered (and thus SNI is not
>> possible).
>>
>> Such a GUC wouldn't make the patch all that much different from what
>> it is
>> right now. What do you think about that middleground proposal?
>
> I like that.
>
> Instead of a boolean GUC, it could perhaps be a path to the pg_hosts
> file. I haven't thought this through but somehow it feels more natural
> to me than a "read this file or not" setting.
I was thinking that the boolean GUC would be called something like
"read_pg_hosts_file = on / off", which feels unnatural. But thinking
about this more, if the GUC is called something like "enable_sni = on /
off", that feels much better, and I like that more than my suggestion of
specifying the path to the pg_hosts file.
- Heikki
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-17 23:58 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Jacob Champion @ 2025-12-17 23:58 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Fri, Dec 12, 2025 at 3:41 AM Daniel Gustafsson <[email protected]> wrote:
> > The comment for HostsLine.ssl_ca, and the code that assigns it,
> > implies to me that host->ssl_ca should never be NULL. Am I missing a
> > case where it could be?
>
> The attached version allows ssl_ca to be omitted from the pg_host config to
> match the ssl_ca GUC.
Aha! I think ssl_ca should be moved into the "Optional fields" section
of `struct HostsLine` now.
> I'm still not sure why they pass for me locally with that error, but I've
> updated to patch to match CI.
There's one diff remaining from my old tests patch: the example.org
line doesn't set ssl_ca, so I expect
> - expected_stderr => qr/unknown ca/);
> + expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
because host_context->ssl_loaded_verify_locations should be false. But
that doesn't happen... Why?
> Adding a boolean GUC which turns ph_hosts (and thus SNI) on or off can perhaps
> fix both complaints?
Sounds reasonable, I think.
--
Just checking my understanding: is the use case for no_sni primarily
that you should be able to strictly refuse clients who say they're
talking to someone else -- so you don't want a wildcard -- but you
still want to gracefully handle clients who don't speak SNI at all?
> + else if (strcmp(host->hostname, "no_sni") == 0)
> + no_sni_context = host_context;
Will anyone be mad at us for camping on the "no_sni" identifier? I
know technically underscore isn't allowed in DNS hostnames, buuuut [1,
2]
> + /* Hostname */
> + field = list_head(tok_line->fields);
> + tokens = lfirst(field);
> + token = linitial(tokens);
> + parsedline->hostname = pstrdup(token->string);
We should probably check tokens->length to make sure that the user
hasn't passed more than one token for each field, similar to how
parse_hba_line() does it.
Should we support multiple hostname tokens in a single line, though,
and just copy the settings that follow across all of them? That would
allow you to collapse
example.org server.crt server.key
example.com server.crt server.key
sub.example.com server.crt server.key
* other.crt other.key
into
example.org,example.com,sub.example.com server.crt server.key
* other.crt other.key
or even
@my-hostnames.txt server.crt server.key
* other.crt other.key
Then you'd have a fighting chance at automatically generating the
lists, especially since we don't do wildcards yet.
--Jacob
[1] https://github.com/netty/netty/pull/8150
[2] https://github.com/openssl/openssl/issues/12566
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-18 00:07 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2025-12-18 00:07 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 18 Dec 2025, at 00:58, Jacob Champion <[email protected]> wrote:
>
> On Fri, Dec 12, 2025 at 3:41 AM Daniel Gustafsson <[email protected]> wrote:
>>> The comment for HostsLine.ssl_ca, and the code that assigns it,
>>> implies to me that host->ssl_ca should never be NULL. Am I missing a
>>> case where it could be?
>>
>> The attached version allows ssl_ca to be omitted from the pg_host config to
>> match the ssl_ca GUC.
>
> Aha! I think ssl_ca should be moved into the "Optional fields" section
> of `struct HostsLine` now.
Ah, yes.
>> I'm still not sure why they pass for me locally with that error, but I've
>> updated to patch to match CI.
>
> There's one diff remaining from my old tests patch: the example.org
> line doesn't set ssl_ca, so I expect
>
>> - expected_stderr => qr/unknown ca/);
>> + expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
>
> because host_context->ssl_loaded_verify_locations should be false. But
> that doesn't happen... Why?
I'll have a look.
> Just checking my understanding: is the use case for no_sni primarily
> that you should be able to strictly refuse clients who say they're
> talking to someone else -- so you don't want a wildcard -- but you
> still want to gracefully handle clients who don't speak SNI at all?
Yeah, pretty much.
>> + else if (strcmp(host->hostname, "no_sni") == 0)
>> + no_sni_context = host_context;
>
> Will anyone be mad at us for camping on the "no_sni" identifier? I
> know technically underscore isn't allowed in DNS hostnames, buuuut [1,
> 2]
Maybe, but I think that regardless of what we do someone will be mad. The
other option would be to use another single character like '?' or something.
Not sure that will improve readability though.
>> + /* Hostname */
>> + field = list_head(tok_line->fields);
>> + tokens = lfirst(field);
>> + token = linitial(tokens);
>> + parsedline->hostname = pstrdup(token->string);
>
> We should probably check tokens->length to make sure that the user
> hasn't passed more than one token for each field, similar to how
> parse_hba_line() does it.
Good point, will do that.
> Should we support multiple hostname tokens in a single line, though,
> and just copy the settings that follow across all of them?
I've been hesitant to add too much complexity, but perhaps just allowing a
comma separated list is a good middle ground to avoid going full regex?
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-18 17:06 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Jacob Champion @ 2025-12-18 17:06 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, Dec 17, 2025 at 4:07 PM Daniel Gustafsson <[email protected]> wrote:
> > Will anyone be mad at us for camping on the "no_sni" identifier? I
> > know technically underscore isn't allowed in DNS hostnames, buuuut [1,
> > 2]
>
> Maybe, but I think that regardless of what we do someone will be mad. The
> other option would be to use another single character like '?' or something.
> Not sure that will improve readability though.
Hm, I agree that's not readable. Especially since other famous server
implementations use ? to match a single character in server alias
names.
Maybe we could enclose no_sni with something that's emphatically not
DNS. Braces, brackets, etc.? If we had control over the lower level
tokenizer, we could tell people to double-quote it to disambiguate,
but I don't think we have access to that information at our level.
> > Should we support multiple hostname tokens in a single line, though,
> > and just copy the settings that follow across all of them?
>
> I've been hesitant to add too much complexity, but perhaps just allowing a
> comma separated list is a good middle ground to avoid going full regex?
I think it could be a pretty good bump in usability. Wildcards seem
ideal but the cost is much higher. Hopefully the cost of
comma-separated hosts is just an extra inner loop in the parser, plus
the extra tests?
I'm trying to put on my "what could we possibly regret" hat for these
next ones. They may be uselessly speculative:
- If the goal is to eventually support wildcards, will the use of a
bare catch-all asterisk conflict with your plans (if any)?
- What kind of normalization should we do? Currently, `example.com`
will not match `example.COM` and it seems like that might be a problem
for somebody.
- Do we need to consider IDNs and A-labels and U-labels? (Do we
support the latter today, at all?)
A nice-to-have v2ish feature might be to warn if the host configured
for a certificate cannot in fact match that certificate according to
OpenSSL.
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2025-12-18 18:20 Jacob Champion <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Jacob Champion @ 2025-12-18 18:20 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Thu, Dec 18, 2025 at 9:06 AM Jacob Champion
<[email protected]> wrote:
> A nice-to-have v2ish feature might be to warn if the host configured
> for a certificate cannot in fact match that certificate according to
> OpenSSL.
Another wishlist item: the logs (both server- and client-side) are
pretty inscrutable when things fail right now. Server's relatively
easy to change, but I wonder if we can do something along the lines of
0b5d1fb36 to provide an extra hint on the client side?
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-01-13 09:57 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-01-13 09:57 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
>> The attached version allows ssl_ca to be omitted from the pg_host config to
>> match the ssl_ca GUC.
>
> Aha! I think ssl_ca should be moved into the "Optional fields" section
> of `struct HostsLine` now.
Done, and removed the now unused default_host member from the struct as well
which I had missed in the previous version.
> We should probably check tokens->length to make sure that the user
> hasn't passed more than one token for each field, similar to how
> parse_hba_line() does it.
Done.
>>> Will anyone be mad at us for camping on the "no_sni" identifier? I
>>> know technically underscore isn't allowed in DNS hostnames, buuuut [1,
>>> 2]
>>
>> Maybe, but I think that regardless of what we do someone will be mad. The
>> other option would be to use another single character like '?' or something.
>> Not sure that will improve readability though.
>
> Hm, I agree that's not readable. Especially since other famous server
> implementations use ? to match a single character in server alias
> names.
>
> Maybe we could enclose no_sni with something that's emphatically not
> DNS. Braces, brackets, etc.? If we had control over the lower level
> tokenizer, we could tell people to double-quote it to disambiguate,
> but I don't think we have access to that information at our level.
I've changed to /no_sni/ in the attached patch which should make it safer, but
it can easily be changed to braces or brackets or something else entirely.
>>> Should we support multiple hostname tokens in a single line, though,
>>> and just copy the settings that follow across all of them?
>>
>> I've been hesitant to add too much complexity, but perhaps just allowing a
>> comma separated list is a good middle ground to avoid going full regex?
>
> I think it could be a pretty good bump in usability. Wildcards seem
> ideal but the cost is much higher. Hopefully the cost of
> comma-separated hosts is just an extra inner loop in the parser, plus
> the extra tests?
I've added support for lists of hostnames along with tests and docs for the
same. The limitation is that one cannot specify '*' or '/no_sni/' in a list,
it must be just hostnames. I haven't added support for @hostnames.txt yet to
keep scope under control, but it can be added as well (in the future if this
patch is committed).
> I'm trying to put on my "what could we possibly regret" hat for these
> next ones. They may be uselessly speculative:
I really appreciate thinking about this!
> - If the goal is to eventually support wildcards, will the use of a
> bare catch-all asterisk conflict with your plans (if any)?
Possibly, I guess it depends on how we define a wildcard scheme. One solution
could perhaps be to use an enclosed name like the non-SNI case, like /default/
or something similar.
> - What kind of normalization should we do? Currently, `example.com`
> will not match `example.COM` and it seems like that might be a problem
> for somebody.
The attached use case insensitive comparison. RFC 952 makes it clear that
hostnames are case insensitive, and RFC 921/1035 does the same for DNS.
> - Do we need to consider IDNs and A-labels and U-labels? (Do we
> support the latter today, at all?)
There is nothing in the current patch which prevents supporting it in a future
update is there?
> A nice-to-have v2ish feature might be to warn if the host configured
> for a certificate cannot in fact match that certificate according to
> OpenSSL.
That would be quite nifty indeed.
I think the attached is pretty clear improvement over the previous version so
thanks for the review suggestions. That being said, the test which was
reported to still fail upstream is failing here as well (it does the right
thing with the connection, but terminates the handshake in a different place).
In an attempt to fix that I moved to using the ClientHello callback which
OpenSSL document to be the right one (yet they use the servername callback
themselves), but it renders the same result. I hope that your eagle eyes (or
someone elses) can figure out either what is wrong, or if this is a different
form of right. The same failing test is added to 0001 to run it in a strictly
non-SNI config as well.
The attached also simplifies the tests you provided since there is no longer
any need to run the tests for different default values, as we no longer have
that mixed configfile handling it was intended to test. The actual connection
tests remain though.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v14-0001-Serverside-SNI-support-for-libpq.patch (59.9K, 2-v14-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From fb0353a10ea352619d585b09776860c19936978c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 11 Dec 2025 18:38:00 +0100
Subject: [PATCH v14 1/2] Serverside SNI support for libpq
Support for SNI was added to clientside libpq in 5c55dc8b4733 with the
sslsni parameter, but there was no support for utilizing it serverside.
This adds support for serverside SNI such that certificate/key handling
is available per host. A new config file, $datadir/pg_hosts.conf, is
used for configuring which certificate and key should be used for which
hostname. If pg_hosts.conf is non-empty it will take precedence over
the regular SSL GUCs, if it is empty or missing the regular GUCs will
be used just as before this commit with no hostname specific handling.
Host configuration can either be for a literal hostname to match, non-
SNI connections using the no_sni keyword or a default fallback matching
all connections. By omitting no_sni and the fallback a strict mode
can be achieved where only connections using sslsni=1 and a specified
hostname are allowed.
CRL file(s) are applied from postgresql.conf to all configured hostnames.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Dewei Dai <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/runtime.sgml | 118 +++++
src/backend/Makefile | 2 +
src/backend/libpq/be-secure-common.c | 198 ++++++++-
src/backend/libpq/be-secure-openssl.c | 415 ++++++++++++++++--
src/backend/libpq/be-secure.c | 6 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 4 +
src/backend/utils/misc/guc.c | 31 ++
src/backend/utils/misc/guc_parameters.dat | 7 +
src/backend/utils/misc/guc_tables.c | 1 +
src/backend/utils/misc/postgresql.conf.sample | 2 +
src/bin/initdb/initdb.c | 15 +-
src/include/libpq/hba.h | 27 ++
src/include/libpq/libpq-be.h | 3 +-
src/include/libpq/libpq.h | 3 +-
src/include/utils/guc.h | 1 +
.../ssl_passphrase_func.c | 4 +-
src/test/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/001_ssltests.pl | 6 +-
src/test/ssl/t/004_sni.pl | 289 ++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 +-
src/tools/pgindent/typedefs.list | 3 +
23 files changed, 1120 insertions(+), 68 deletions(-)
create mode 100644 src/backend/libpq/pg_hosts.conf.sample
create mode 100644 src/test/ssl/t/004_sni.pl
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..ca0a114da76 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,118 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for Server Name
+ Indication, <acronym>SNI</acronym>, using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection based on
+ the hosts which are defined in <filename>pg_hosts.conf</filename>.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the cluster's
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+
+ <para>
+ <replaceable>hostname</replaceable> should either be set to the literal
+ hostname for the connection, <literal>no_sni</literal> or <literal>*</literal>.
+ <xref linkend="hostname-values"/> contain details on how these values are
+ used.
+ <table id="hostname-values">
+ <title>Hostname setting values</title>
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Host Entry</entry>
+ <entry>sslsni</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal>*</literal></entry>
+ <entry>Not required</entry>
+ <entry>Default host, matches all connections</entry>
+ </row>
+
+ <row>
+ <entry><literal>no_sni</literal></entry>
+ <entry>Not allowed</entry>
+ <entry>
+ Certificate and key to use for connection with no <literal>sslsni</literal> defined.
+ </entry>
+ </row>
+
+ <row>
+ <entry><replaceable>hostname</replaceable></entry>
+ <entry>Required</entry>
+ <entry>
+ Certificate and key to use for connections to the host specified in the
+ connection.
+ </entry>
+ </row>
+ </tbody>
+
+ </tgroup>
+ </table>
+ </para>
+
+ <para>
+ If <filename>pg_hosts.conf</filename> is empty, or missing, then the SSL
+ configuration in <filename>postgresql.conf</filename> will be used for all
+ connections. If <filename>pg_hosts.conf</filename> is non-empty then it
+ will take precedence over certificate and key settings in
+ <filename>postgresql.conf</filename>.
+ </para>
+
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+
+ <para>
+ The CRL configuration in <filename>postgresql.conf</filename> is applied
+ on all connections regardless of if they use SNI or not.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index e03c92e70e4..e7ac9c8dcb0 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -209,6 +209,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)
@@ -268,6 +269,7 @@ endif
$(MAKE) -C utils uninstall-data
rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \
'$(DESTDIR)$(datadir)/pg_ident.conf.sample' \
+ '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \
'$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
$(call uninstall_llvm_module,postgres)
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index c074556dbfc..78430aad825 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -24,32 +24,40 @@
#include "common/percentrepl.h"
#include "common/string.h"
+#include "libpq/hba.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
+ const char *cmd = (const char *) userdata;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +183,187 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->hostname = pstrdup(token->string);
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (optional) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ return parsedline;
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ if (token->string[0] == '1'
+ || pg_strcasecmp(token->string, "true") == 0
+ || pg_strcasecmp(token->string, "on") == 0
+ || pg_strcasecmp(token->string, "yes") == 0)
+ parsedline->ssl_passphrase_reload = true;
+ else if (token->string[0] == '0'
+ || pg_strcasecmp(token->string, "false") == 0
+ || pg_strcasecmp(token->string, "off") == 0
+ || pg_strcasecmp(token->string, "no") == 0)
+ parsedline->ssl_passphrase_reload = false;
+ else
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * file. The list is returned in the hosts_lines parameter. If loading the
+ * file was successful, true is returned, else false. This function is
+ * intended to be executed within a temporary memory context which can be
+ * discarded to free memory allocated during the processing of the file.
+ */
+int
+load_hosts(List **hosts, char **err_msg)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ /*
+ * If we cannot return results then error out immediately. This implies
+ * API misuse or a similar kind of programmer error.
+ */
+ if (!hosts)
+ return HOSTSFILE_LOAD_FAILED;
+ *hosts = NIL;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, err_msg);
+ if (file == NULL)
+ {
+ if (errno == ENOENT)
+ return HOSTSFILE_MISSING;
+
+ return HOSTSFILE_LOAD_FAILED;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ if ((tok_line->err_msg != NULL) ||
+ ((newline = parse_hosts_line(tok_line, LOG)) == NULL))
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ free_auth_file(file, 0);
+ *hosts = parsed_lines;
+
+ if (!ok)
+ return HOSTSFILE_LOAD_FAILED;
+
+ if (parsed_lines == NIL)
+ return HOSTSFILE_EMPTY;
+
+ return HOSTSFILE_LOAD_OK;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 4da6ac22ff9..a540cc93163 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,15 @@
#endif
#include <openssl/x509v3.h>
+typedef struct HostContext
+{
+ const char *hostname;
+ SSL_CTX *context;
+ bool ssl_loaded_verify_locations;
+} HostContext;
/* default init hook can be overridden by a shared library */
-static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
+static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts);
openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init;
static int port_bio_read(BIO *h, char *buf, int size);
@@ -73,6 +79,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
+static int sni_servername_cb(SSL *ssl, int *al, void *arg);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
@@ -80,12 +87,26 @@ static const char *SSLerrmessage(unsigned long ecode);
static char *X509_NAME_to_cstring(X509_NAME *name);
+/* List of SSL contexts for hostname defined connections */
+static List *sni_contexts = NIL;
+
+/* The default SSL context to use as fallback in case no hostname matches */
+static HostContext *default_context = NULL;
+
+/* The SSL context to use for connections without SNI */
+static HostContext *no_sni_context = NULL;
+
+/* The currently active context */
static SSL_CTX *SSL_context = NULL;
+static HostContext *Host_context = NULL;
+
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
+static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host);
+static void free_contexts(void);
struct CallbackErr
{
@@ -102,11 +123,181 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
+{
+ List *pg_hosts = NIL;
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext host_memcxt;
+ char *err_msg;
+ int res;
+
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (sni_contexts != NIL || default_context || no_sni_context)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ }
+
+ /*
+ * Attempt to load, and parse, TLS configuration from the pg_hosts.conf
+ * file with the set of hosts returned as a list. If there are hosts
+ * configured there they take precedence over the postgresql.conf config.
+ * Make sure to allocate the parsed rows in a temporary memory context so
+ * that we can avoid memory leaks from the parsing process.
+ */
+ host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(host_memcxt);
+ res = load_hosts(&pg_hosts, &err_msg);
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * pg_hosts.conf is not required to contain configuration, but if it does
+ * we error out in case it fails to load rather than continue to try the
+ * postgresql.conf configuration to avoid silently falling back on an
+ * undesired configuration.
+ */
+ if (res == HOSTSFILE_LOAD_FAILED)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load \"%s\": %s", "pg_hosts.conf",
+ err_msg ? err_msg : "unknown error"));
+ MemoryContextDelete(host_memcxt);
+ return -1;
+ }
+
+ /*
+ * Loading and parsing the hosts file was successful, create contexts for
+ * each host entry and add to the list of hosts to be checked during
+ * login.
+ */
+ else if (res == HOSTSFILE_LOAD_OK)
+ {
+ foreach(line, pg_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+ SSL_CTX *tmp_context = NULL;
+
+ tmp_context = ssl_init_context(isServerStart, host);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load SSL config from \"%s\" line %i",
+ host->sourcefile, host->linenumber));
+ free_contexts();
+ MemoryContextDelete(host_memcxt);
+ return -1;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+ host_context->context = tmp_context;
+
+ /* Set flag to remember whether CA store has been loaded */
+ if (host->ssl_ca && host->ssl_ca[0] != '\0')
+ host_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * The hostname in the context is NULL in case it is the default
+ * host, or a context to use for non-SNI connections.
+ */
+ if (strcmp(host->hostname, "*") == 0)
+ default_context = host_context;
+ else if (strcmp(host->hostname, "no_sni") == 0)
+ no_sni_context = host_context;
+ else
+ {
+ host_context->hostname = pstrdup(host->hostname);
+ sni_contexts = lappend(sni_contexts, host_context);
+ }
+
+ /*
+ * There needs to be an installed context to drive the handshake
+ * until the SNI callback switches over to the expected one, for
+ * now just set it to the first one we see.
+ */
+ if (!Host_context)
+ Host_context = host_context;
+ }
+
+ MemoryContextDelete(host_memcxt);
+ }
+
+ /*
+ * If the pg_hosts.conf file doesn't exist, or is empty, then load the
+ * config from postgresql.conf.
+ */
+ else if (res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
+ {
+ HostsLine pgconf;
+ SSL_CTX *tmp_context = NULL;
+
+ memset(&pgconf, 0, sizeof(pgconf));
+ pgconf.ssl_cert = ssl_cert_file;
+ pgconf.ssl_key = ssl_key_file;
+ pgconf.ssl_ca = ssl_ca_file;
+ pgconf.ssl_passphrase_cmd = ssl_passphrase_command;
+ pgconf.ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ tmp_context = ssl_init_context(isServerStart, &pgconf);
+ if (tmp_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load SSL configuration from \"%s\"",
+ "postgresql.conf"));
+ return -1;
+ }
+
+ /*
+ * If postgresql.conf is used to configure SSL then by definition it
+ * will be the default context as we don't have per-host config. We
+ * can also set it as the Host_context since it will be used for all
+ * connections.
+ */
+ default_context = palloc0(sizeof(HostContext));
+ default_context->context = tmp_context;
+ Host_context = default_context;
+
+ /* Set flag to remember whether CA store has been loaded */
+ if (ssl_ca_file[0])
+ default_context->ssl_loaded_verify_locations = true;
+ }
+
+ /* Make sure we have at least one certificate loaded */
+ if (sni_contexts == NIL && !default_context && !no_sni_context)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL contexts loaded"));
+ return -1;
+ }
+
+ SSL_context = Host_context->context;
+
+ return 0;
+}
+
+static SSL_CTX *
+ssl_init_context(bool isServerStart, HostsLine *host_line)
{
SSL_CTX *context;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ const char *ctx_ssl_cert_file = host_line->ssl_cert;
+ const char *ctx_ssl_key_file = host_line->ssl_key;
+ const char *ctx_ssl_ca_file = host_line->ssl_ca;
+
/*
* Create a new SSL context into which we'll load all the configuration
* settings. If we fail partway through, we can avoid memory leakage by
@@ -132,10 +323,16 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ /*
+ * Install SNI TLS extension callback in order to validate hostnames in
+ * case we have at least one context configured with a host name.
+ */
+ SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
+
/*
* Call init hook (usually to set password callback)
*/
- (*openssl_tls_init_hook) (context, isServerStart);
+ (*openssl_tls_init_hook) (context, isServerStart, host_line);
/* used by the callback */
ssl_is_server_start = isServerStart;
@@ -143,16 +340,16 @@ be_tls_init(bool isServerStart)
/*
* Load and verify server's certificate and private key
*/
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
+ if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_cert_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
+ if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart))
goto error;
/*
@@ -161,19 +358,19 @@ be_tls_init(bool isServerStart)
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
+ ctx_ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (dummy_ssl_passwd_cb_called)
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
+ ctx_ssl_key_file)));
else
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_key_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -325,17 +522,17 @@ be_tls_init(bool isServerStart)
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (ctx_ssl_ca_file && ctx_ssl_ca_file[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ ctx_ssl_ca_file, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -347,18 +544,17 @@ be_tls_init(bool isServerStart)
* free it when no longer needed.
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
-
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
- SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
- verify_cb);
}
+ /*
+ * Always ask for SSL client cert, but don't fail if it's not presented.
+ * We might fail such connections later, depending on what we find in
+ * pg_hba.conf.
+ */
+ SSL_CTX_set_verify(context,
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+ verify_cb);
+
/*----------
* Load the Certificate Revocation List (CRL).
* http://searchsecurity.techtarget.com/sDefinition/0,,sid14_gci803160,00.html
@@ -407,38 +603,19 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
-
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
+ return context;
/* Clean up by releasing working context. */
error:
if (context)
SSL_CTX_free(context);
- return -1;
+ return NULL;
}
void
be_tls_destroy(void)
{
- if (SSL_context)
- SSL_CTX_free(SSL_context);
- SSL_context = NULL;
- ssl_loaded_verify_locations = false;
+ free_contexts();
}
int
@@ -771,6 +948,9 @@ be_tls_close(Port *port)
pfree(port->peer_dn);
port->peer_dn = NULL;
}
+
+ Host_context = NULL;
+ SSL_context = NULL;
}
ssize_t
@@ -1144,7 +1324,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata);
}
/*
@@ -1390,6 +1570,92 @@ alpn_cb(SSL *ssl,
}
}
+/*
+ * sni_servername_cb
+ *
+ * Callback executed by OpenSSL during handshake in case the server has been
+ * configured to validate hostnames. Returning SSL_TLSEXT_ERR_ALERT_FATAL to
+ * OpenSSL will immediately terminate the handshake.
+ */
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ HostContext *install_context = NULL;
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ /*
+ * If there is no hostname set in the TLS extension, we have two options:
+ * i) there is a HostContext defined for non-SNI connections, in that case
+ * we switch to that; ii) there is no non-SNI config and we error out as
+ * there is no context to switch to.
+ */
+ if (!tlsext_hostname)
+ {
+ if (no_sni_context)
+ install_context = no_sni_context;
+ else if (default_context)
+ install_context = default_context;
+ else
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback")));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ }
+ else
+ {
+ /*
+ * We have a requested hostname from the client, match against all
+ * entries in the pg_hosts configuration and attempt to find a match.
+ */
+ foreach_ptr(HostContext, host, sni_contexts)
+ {
+ if (strcmp(host->hostname, tlsext_hostname) == 0)
+ {
+ install_context = host;
+ break;
+ }
+ }
+
+ /*
+ * If no host specific match was found, and there is a default config,
+ * then fall back to using that.
+ */
+ if (!install_context && default_context)
+ install_context = default_context;
+ }
+
+ /*
+ * If we reach here without a context chosen as the session context then
+ * fail the handshake and terminate the connection.
+ */
+ if (install_context == NULL)
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+
+ Host_context = install_context;
+ SSL_context = install_context->context;
+ if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to SSL context for host"));
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1599,6 +1865,14 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
ptr[0] = '\0';
}
+bool
+be_tls_loaded_verify_locations(void)
+{
+ if (!Host_context)
+ return false;
+ return Host_context->ssl_loaded_verify_locations;
+}
+
char *
be_tls_get_certificate_hash(Port *port, size_t *len)
{
@@ -1792,17 +2066,23 @@ ssl_protocol_version_to_string(int v)
static void
-default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
+default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
if (isServerStart)
{
- if (ssl_passphrase_command[0])
+ if (host->ssl_passphrase_cmd != NULL)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
}
else
{
- if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd);
+ }
else
/*
@@ -1814,3 +2094,42 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+/*
+ * Cleanup function for when hostname configuration is reloaded from the
+ * pg_hosts.conf file, at that point we must discard all existing contexts.
+ */
+static void
+free_contexts(void)
+{
+ if (sni_contexts != NIL)
+ {
+ foreach_ptr(HostContext, host, sni_contexts)
+ {
+ if (host->hostname)
+ pfree(unconstify(char *, host->hostname));
+ SSL_CTX_free(host->context);
+ }
+
+ list_free_deep(sni_contexts);
+ sni_contexts = NIL;
+ }
+
+ /*
+ * The hostname need not be freed for the no_sni and default contexts
+ * since they by definition are not connected to a hostname and thus have
+ * none allocated.
+ */
+ if (no_sni_context)
+ {
+ SSL_CTX_free(no_sni_context->context);
+ pfree(no_sni_context);
+ no_sni_context = NULL;
+ }
+ if (default_context)
+ {
+ SSL_CTX_free(default_context->context);
+ pfree(default_context);
+ default_context = NULL;
+ }
+}
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 3f9257ab010..6dcb673843a 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -43,10 +43,6 @@ char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
-#ifdef USE_SSL
-bool ssl_loaded_verify_locations = false;
-#endif
-
/* GUC variable controlling SSL cipher list */
char *SSLCipherSuites = NULL;
char *SSLCipherList = NULL;
@@ -99,7 +95,7 @@ bool
secure_loaded_verify_locations(void)
{
#ifdef USE_SSL
- return ssl_loaded_verify_locations;
+ return be_tls_loaded_verify_locations();
#else
return false;
#endif
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 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 ae9d5f3fb70..360b08b5853 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,36 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..b95c373fb41 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1176,6 +1176,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 73ff6ad0a32..a88690933c7 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -556,6 +556,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..1f360110564 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index a3980e5535f..023ef134856 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;
@@ -1532,6 +1533,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);
@@ -2793,6 +2802,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");
@@ -2808,12 +2818,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);
}
@@ -2821,6 +2831,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7b93ba4a709..38713381255 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,33 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ bool default_host;
+ char *hostname;
+ char *ssl_key;
+ char *ssl_cert;
+ char *ssl_ca;
+
+ /* Optional fields */
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+} HostsLine;
+
+typedef enum HostsFileLoad
+{
+ HOSTSFILE_LOAD_OK = 0,
+ HOSTSFILE_LOAD_FAILED,
+ HOSTSFILE_EMPTY,
+ HOSTSFILE_MISSING,
+} HostsFileLoadResult;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..c85e631cda2 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 412bc9758fb..3d734266172 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -156,8 +156,9 @@ enum ssl_protocol_versions
* prototypes for functions in be-secure-common.c
*/
extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
- char *buf, int size);
+ char *buf, int size, void *userdata);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern int load_hosts(List **hosts, char **err_msg);
#endif /* LIBPQ_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index bf39878c43e..f97d93136ed 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
index d5992149821..a85d85735cf 100644
--- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
+++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c
@@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL;
static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata);
/* hook function to set the callback */
-static void set_rot13(SSL_CTX *context, bool isServerStart);
+static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host);
/*
* Module load callback
@@ -53,7 +53,7 @@ _PG_init(void)
}
static void
-set_rot13(SSL_CTX *context, bool isServerStart)
+set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host)
{
/* warn if the user has set ssl_passphrase_command */
if (ssl_passphrase_command[0])
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 955dfc0e7f8..3a3088f756b 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 2b9b3dfd663..c0104f6aa81 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -380,11 +380,11 @@ switch_server_cert($node, certfile => 'server-ip-cn-only');
$common_connstr =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in the Common Name");
$node->connect_fails(
- "$common_connstr host=192.000.002.001",
+ "$common_connstr host=192.000.002.001 sslsni=0",
"mismatch between host name and server certificate IP address",
expected_stderr =>
qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
@@ -394,7 +394,7 @@ $node->connect_fails(
# long-standing behavior.)
switch_server_cert($node, certfile => 'server-ip-in-dnsname');
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in a dNSName");
# Test Subject Alternative Names.
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..2dd70e7afee
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,289 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostaddr used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR sslsni=1";
+
+##############################################################################
+# postgresql.conf
+##############################################################################
+
+# Connect without any hosts configured in pg_hosts.conf, thus using the cert
+# and key in postgresql.conf. pg_hosts.conf exists at this point but is empty
+# apart from the comments stemming from the sample.
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg.conf: connect fails without intermediate for sslmode=verify-ca",
+ expected_stderr => qr/certificate verify failed/);
+
+# Remove pg_hosts.conf and reload to make sure a missing file is treated like
+# an empty file.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect after deleting pg_hosts.conf");
+
+##############################################################################
+# pg_hosts.conf
+##############################################################################
+
+# Replicate the postgresql.conf configuration into pg_hosts.conf and retry the
+# same tests as above.
+$node->append_conf('pg_hosts.conf',
+ "* server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg_hosts.conf: connect to default, with correct server CA cert file sslmode=require"
+);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to default, fail without intermediate for sslmode=verify-ca",
+ expected_stderr => qr/certificate verify failed/);
+
+# Add host entry for example.org which serves the server cert and its
+# intermediate CA. The previously existing default host still exists without
+# a CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org and verify server CA");
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=invalid sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org but without server root cert, sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to default and fail to verify CA",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg_hosts.conf: connect to default with sslmode=require");
+
+# Modify pg_hosts.conf to no longer have the default host entry.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+# Connecting without a hostname as well as with a hostname which isn't in the
+# pg_hosts configuration should fail.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/missing extension/);
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.com",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+);
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file after more reloads"
+);
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Connecting after restart should succeed but not after the
+# following reload.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+ );
+}
+
+$node->reload;
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect fails since the passphrase protected key cannot be reloaded"
+);
+
+# Configure with only non-SNI connections allowed
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "no_sni server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "pg_hosts.conf: only non-SNI connections allowed");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.org",
+ "pg_hosts.conf: only non-SNI connections allowed, connecting with SNI",
+ expected_stderr => qr/unrecognized name/);
+
+# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
+# time swapping out default contexts containing different CA configurations.
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+foreach my $default_ca ("", "root+client_ca", "root+server_ca")
+{
+ # The default CA should, not matter for the purposes of these tests, since
+ # we connect to the other hosts explicitly. Test with various default CA
+ # settings to ensure it's isolated from the actual connections.
+ $ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ cafile => $default_ca);
+
+ # example.org is unconfigured and should fail.
+ $node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
+ expected_stderr => qr/unknown ca/);
+
+ # example.com is configured and should require a valid client cert.
+ $node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/
+ );
+
+ $node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
+ );
+
+ # example.net is configured and should require a client cert, but will
+ # always fail verification.
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/
+ );
+
+ $node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
+ expected_stderr => qr/unknown ca/);
+}
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 7ea05572a8d..6060771c1a8 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 14dec2d49c1..e6c9155a186 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1220,6 +1220,9 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsFileLoadResult
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
[application/octet-stream] v14-0002-Review-comments.patch (37.5K, 3-v14-0002-Review-comments.patch)
download | inline diff:
From d8033be991487f2bc80b5ae26b175014f66e9d3d Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 19 Dec 2025 12:02:24 +0100
Subject: [PATCH v14 2/2] Review comments
---
doc/src/sgml/runtime.sgml | 19 +-
src/backend/libpq/be-secure-common.c | 47 ++-
src/backend/libpq/be-secure-openssl.c | 276 ++++++++++++------
src/backend/libpq/be-secure.c | 3 +
src/backend/utils/misc/guc_parameters.dat | 7 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/libpq/hba.h | 6 +-
src/include/libpq/libpq.h | 1 +
src/test/ssl/t/001_ssltests.pl | 41 ++-
src/test/ssl/t/004_sni.pl | 164 +++++++----
10 files changed, 397 insertions(+), 168 deletions(-)
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index ca0a114da76..0705b72ca4e 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2626,8 +2626,8 @@ openssl x509 -req -in server.csr -text -days 365 \
<para>
<replaceable>hostname</replaceable> should either be set to the literal
- hostname for the connection, <literal>no_sni</literal> or <literal>*</literal>.
- <xref linkend="hostname-values"/> contain details on how these values are
+ 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>
@@ -2644,14 +2644,17 @@ openssl x509 -req -in server.csr -text -days 365 \
<row>
<entry><literal>*</literal></entry>
<entry>Not required</entry>
- <entry>Default host, matches all connections</entry>
+ <entry>
+ Default host, matches all connections.
+ </entry>
</row>
<row>
- <entry><literal>no_sni</literal></entry>
+ <entry><literal>/no_sni/</literal></entry>
<entry>Not allowed</entry>
<entry>
- Certificate and key to use for connection with no <literal>sslsni</literal> defined.
+ Certificate and key to use for connections with no
+ <literal>sslsni</literal> defined.
</entry>
</row>
@@ -2659,8 +2662,10 @@ openssl x509 -req -in server.csr -text -days 365 \
<entry><replaceable>hostname</replaceable></entry>
<entry>Required</entry>
<entry>
- Certificate and key to use for connections to the host specified in the
- connection.
+ 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>
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index 78430aad825..251100c27b8 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -204,6 +204,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
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;
@@ -212,8 +213,21 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
/* Hostname */
field = list_head(tok_line->fields);
tokens = lfirst(field);
- token = linitial(tokens);
- parsedline->hostname = pstrdup(token->string);
+ 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);
@@ -227,6 +241,15 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
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);
@@ -242,6 +265,15 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
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);
@@ -250,6 +282,15 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
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);
@@ -301,7 +342,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
* load_hosts
*
* Reads and parses the pg_hosts.conf configuration file and passes back a List
- * of HostLine elements containing the parsed lines, or NIL in case of an empty
+ * of HostsLine elements containing the parsed lines, or NIL in case of an empty
* file. The list is returned in the hosts_lines parameter. If loading the
* file was successful, true is returned, else false. This function is
* intended to be executed within a temporary memory context which can be
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index a540cc93163..c60bc2209b5 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -53,7 +53,7 @@
typedef struct HostContext
{
- const char *hostname;
+ List *hostnames;
SSL_CTX *context;
bool ssl_loaded_verify_locations;
} HostContext;
@@ -79,7 +79,7 @@ static int alpn_cb(SSL *ssl,
const unsigned char *in,
unsigned int inlen,
void *userdata);
-static int sni_servername_cb(SSL *ssl, int *al, void *arg);
+static int sni_clienthello_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);
@@ -130,33 +130,31 @@ be_tls_init(bool isServerStart)
MemoryContext host_memcxt;
char *err_msg;
int res;
+ List *new_sni_contexts = NIL;
+ HostContext *new_default_context = NULL;
+ HostContext *new_no_sni_context = NULL;
+ HostContext *new_Host_context = NULL;
/*
- * If there are contexts loaded when we init they must be released. This
- * should only be possible during configuration reloads and not when the
- * server is starting up.
+ * If 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
+ * postgresql.conf config. Make sure to allocate the parsed rows in a
+ * temporary memory context so that we can avoid memory leaks from the
+ * parsing process. If ssl_sni is disabled then set the state accordingly
+ * to make sure we instead parse the config from postgresql.conf.
*/
- if (sni_contexts != NIL || default_context || no_sni_context)
+ if (ssl_sni)
{
- Assert(!isServerStart);
- free_contexts();
- Host_context = NULL;
- SSL_context = NULL;
+ host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(host_memcxt);
+ res = load_hosts(&pg_hosts, &err_msg);
+ MemoryContextSwitchTo(oldcxt);
}
-
- /*
- * Attempt to load, and parse, TLS configuration from the pg_hosts.conf
- * file with the set of hosts returned as a list. If there are hosts
- * configured there they take precedence over the postgresql.conf config.
- * Make sure to allocate the parsed rows in a temporary memory context so
- * that we can avoid memory leaks from the parsing process.
- */
- host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
- "hosts file parser context",
- ALLOCSET_SMALL_SIZES);
- oldcxt = MemoryContextSwitchTo(host_memcxt);
- res = load_hosts(&pg_hosts, &err_msg);
- MemoryContextSwitchTo(oldcxt);
+ else
+ res = HOSTSFILE_DISABLED;
/*
* pg_hosts.conf is not required to contain configuration, but if it does
@@ -194,12 +192,13 @@ be_tls_init(bool isServerStart)
errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("unable to load SSL config from \"%s\" line %i",
host->sourcefile, host->linenumber));
- free_contexts();
MemoryContextDelete(host_memcxt);
return -1;
}
- host_context = palloc0(sizeof(HostContext));
+ host_context = palloc(sizeof(HostContext));
+ host_context->hostnames = NIL;
+ host_context->ssl_loaded_verify_locations = false;
host_context->context = tmp_context;
/* Set flag to remember whether CA store has been loaded */
@@ -208,16 +207,21 @@ be_tls_init(bool isServerStart)
/*
* The hostname in the context is NULL in case it is the default
- * host, or a context to use for non-SNI connections.
+ * host, or a context to use for non-SNI connections. We already
+ * know that default and non-SNI configurations are not mixed with
+ * hostnames so in those cases we can just take the head of the
+ * list.
*/
- if (strcmp(host->hostname, "*") == 0)
- default_context = host_context;
- else if (strcmp(host->hostname, "no_sni") == 0)
- no_sni_context = host_context;
+ if (strcmp(linitial(host->hostnames), "*") == 0)
+ new_default_context = host_context;
+ else if (strcmp(linitial(host->hostnames), "/no_sni/") == 0)
+ new_no_sni_context = host_context;
else
{
- host_context->hostname = pstrdup(host->hostname);
- sni_contexts = lappend(sni_contexts, host_context);
+ foreach_ptr(char, hostname, host->hostnames)
+ host_context->hostnames = lappend(host_context->hostnames,
+ pstrdup(hostname));
+ new_sni_contexts = lappend(new_sni_contexts, host_context);
}
/*
@@ -225,8 +229,8 @@ be_tls_init(bool isServerStart)
* until the SNI callback switches over to the expected one, for
* now just set it to the first one we see.
*/
- if (!Host_context)
- Host_context = host_context;
+ if (!new_Host_context)
+ new_Host_context = host_context;
}
MemoryContextDelete(host_memcxt);
@@ -236,7 +240,7 @@ be_tls_init(bool isServerStart)
* If the pg_hosts.conf file doesn't exist, or is empty, then load the
* config from postgresql.conf.
*/
- else if (res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
+ else if (res == HOSTSFILE_DISABLED || res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
{
HostsLine pgconf;
SSL_CTX *tmp_context = NULL;
@@ -264,17 +268,18 @@ be_tls_init(bool isServerStart)
* can also set it as the Host_context since it will be used for all
* connections.
*/
- default_context = palloc0(sizeof(HostContext));
- default_context->context = tmp_context;
- Host_context = default_context;
+ new_default_context = palloc(sizeof(HostContext));
+ new_default_context->context = tmp_context;
+ new_default_context->ssl_loaded_verify_locations = false;
+ new_Host_context = new_default_context;
/* Set flag to remember whether CA store has been loaded */
if (ssl_ca_file[0])
- default_context->ssl_loaded_verify_locations = true;
+ new_default_context->ssl_loaded_verify_locations = true;
}
/* Make sure we have at least one certificate loaded */
- if (sni_contexts == NIL && !default_context && !no_sni_context)
+ if (new_sni_contexts == NIL && !new_default_context && !new_no_sni_context)
{
ereport(isServerStart ? FATAL : LOG,
errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -282,6 +287,24 @@ be_tls_init(bool isServerStart)
return -1;
}
+ /*
+ * If there are contexts loaded when we init they must be released. This
+ * should only be possible during configuration reloads and not when the
+ * server is starting up.
+ */
+ if (sni_contexts != NIL || default_context || no_sni_context)
+ {
+ Assert(!isServerStart);
+ free_contexts();
+ Host_context = NULL;
+ SSL_context = NULL;
+ }
+
+ sni_contexts = new_sni_contexts;
+ no_sni_context = new_no_sni_context;
+ default_context = new_default_context;
+ Host_context = new_Host_context;
+
SSL_context = Host_context->context;
return 0;
@@ -323,12 +346,6 @@ ssl_init_context(bool isServerStart, HostsLine *host_line)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
- /*
- * Install SNI TLS extension callback in order to validate hostnames in
- * case we have at least one context configured with a host name.
- */
- SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb);
-
/*
* Call init hook (usually to set password callback)
*/
@@ -544,16 +561,23 @@ ssl_init_context(bool isServerStart, HostsLine *host_line)
* free it when no longer needed.
*/
SSL_CTX_set_client_CA_list(context, root_cert_list);
+
+ /*
+ * Always ask for SSL client cert, but don't fail if it's not
+ * presented. We might fail such connections later, depending on what
+ * we find in pg_hba.conf.
+ */
+ SSL_CTX_set_verify(context,
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+ verify_cb);
}
/*
- * Always ask for SSL client cert, but don't fail if it's not presented.
- * We might fail such connections later, depending on what we find in
- * pg_hba.conf.
+ * Install SNI TLS extension callback in order to validate hostnames in
+ * case ssl_sni has been enabled.
*/
- SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
- verify_cb);
+ if (ssl_sni)
+ SSL_CTX_set_client_hello_cb(context, sni_clienthello_cb, NULL);
/*----------
* Load the Certificate Revocation List (CRL).
@@ -645,6 +669,10 @@ be_tls_open_server(Port *port)
/* enable ALPN */
SSL_CTX_set_alpn_select_cb(SSL_context, alpn_cb, port);
+ SSL_CTX_set_verify(SSL_context,
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+ verify_cb);
+
if (!(port->ssl = SSL_new(SSL_context)))
{
ereport(COMMERROR,
@@ -1571,61 +1599,83 @@ alpn_cb(SSL *ssl,
}
/*
- * sni_servername_cb
+ * sni_clienthello_cb
*
- * Callback executed by OpenSSL during handshake in case the server has been
- * configured to validate hostnames. Returning SSL_TLSEXT_ERR_ALERT_FATAL to
- * OpenSSL will immediately terminate the handshake.
+ * 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_servername_cb(SSL *ssl, int *al, void *arg)
+sni_clienthello_cb(SSL *ssl, int *al, void *arg)
{
const char *tlsext_hostname;
+ const unsigned char *tlsext;
+ size_t left,
+ len;
HostContext *install_context = NULL;
- tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+ if (!ssl_sni)
+ return SSL_CLIENT_HELLO_SUCCESS;
- /*
- * If there is no hostname set in the TLS extension, we have two options:
- * i) there is a HostContext defined for non-SNI connections, in that case
- * we switch to that; ii) there is no non-SNI config and we error out as
- * there is no context to switch to.
- */
- if (!tlsext_hostname)
+ if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &tlsext, &left))
{
- if (no_sni_context)
- install_context = no_sni_context;
- else if (default_context)
- install_context = default_context;
- else
+ if (left <= 2)
+ {
+ *al = SSL_AD_MISSING_EXTENSION;
+ return 0;
+ }
+ len = (*(tlsext++) << 8);
+ len += *(tlsext)++;
+ if (len + 2 != left)
{
- /*
- * 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;
+ return 0;
+ }
- ereport(COMMERROR,
- (errcode(ERRCODE_PROTOCOL_VIOLATION),
- errmsg("no hostname provided in callback")));
- return SSL_TLSEXT_ERR_ALERT_FATAL;
+ left = len;
+
+ if (left == 0 || *tlsext++ != TLSEXT_NAMETYPE_host_name)
+ {
+ *al = SSL_AD_MISSING_EXTENSION;
+ return 0;
}
- }
- else
- {
+
+ 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(HostContext, host, sni_contexts)
{
- if (strcmp(host->hostname, tlsext_hostname) == 0)
+ foreach_ptr(char, hostname, host->hostnames)
{
- install_context = host;
- break;
+ if (pg_strncasecmp(hostname, tlsext_hostname, len) == 0)
+ {
+ install_context = host;
+ goto found;
+ }
}
}
@@ -1636,14 +1686,44 @@ sni_servername_cb(SSL *ssl, int *al, void *arg)
if (!install_context && default_context)
install_context = default_context;
}
+ else
+ {
+ if (no_sni_context)
+ install_context = no_sni_context;
+ else if (default_context)
+ install_context = default_context;
+ else
+ {
+ /*
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback, 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_context == NULL)
- return SSL_TLSEXT_ERR_ALERT_FATAL;
+ {
+ if (tlsext_hostname)
+ *al = SSL_AD_UNRECOGNIZED_NAME;
+ else
+ *al = SSL_AD_MISSING_EXTENSION;
+ return SSL_CLIENT_HELLO_ERROR;
+ }
+found:
Host_context = install_context;
SSL_context = install_context->context;
if (SSL_set_SSL_CTX(ssl, SSL_context) == NULL)
@@ -1651,10 +1731,14 @@ sni_servername_cb(SSL *ssl, int *al, void *arg)
ereport(COMMERROR,
errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("failed to switch to SSL context for host"));
- return SSL_TLSEXT_ERR_ALERT_FATAL;
+ return SSL_CLIENT_HELLO_ERROR;
}
- return SSL_TLSEXT_ERR_OK;
+ /* Copy over context settings */
+ SSL_clear_options(ssl, 0xFFFFFFFFL);
+ SSL_set_options(ssl, SSL_CTX_get_options(SSL_context));
+
+ return SSL_CLIENT_HELLO_SUCCESS;
}
/*
@@ -2106,8 +2190,12 @@ free_contexts(void)
{
foreach_ptr(HostContext, host, sni_contexts)
{
- if (host->hostname)
- pfree(unconstify(char *, host->hostname));
+ if (host->hostnames != NIL)
+ {
+ foreach_ptr(char, hostname, host->hostnames)
+ pfree(hostname);
+ list_free(host->hostnames);
+ }
SSL_CTX_free(host->context);
}
@@ -2116,7 +2204,7 @@ free_contexts(void)
}
/*
- * The hostname need not be freed for the no_sni and default contexts
+ * The hostname list need not be freed for the no_sni and default contexts
* since they by definition are not connected to a hostname and thus have
* none allocated.
*/
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 6dcb673843a..542aaaa2b26 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -56,6 +56,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/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index b95c373fb41..2765e6bbf4a 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2758,6 +2758,13 @@
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',
+},
+
{ 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/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 1f360110564..404621df9bf 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -123,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_sni = off
#------------------------------------------------------------------------------
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 38713381255..9d35ea1ba63 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -159,13 +159,12 @@ typedef struct HostsLine
char *rawline;
/* Required fields */
- bool default_host;
- char *hostname;
+ List *hostnames;
char *ssl_key;
char *ssl_cert;
- char *ssl_ca;
/* Optional fields */
+ char *ssl_ca;
char *ssl_passphrase_cmd;
bool ssl_passphrase_reload;
} HostsLine;
@@ -176,6 +175,7 @@ typedef enum HostsFileLoad
HOSTSFILE_LOAD_FAILED,
HOSTSFILE_EMPTY,
HOSTSFILE_MISSING,
+ HOSTSFILE_DISABLED,
} HostsFileLoadResult;
/*
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 3d734266172..47009ead442 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -110,6 +110,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;
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index c0104f6aa81..6a433623d1f 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -201,7 +201,7 @@ $result = $node->restart(fail_ok => 1);
is($result, 0, 'restart fails with incorrect groups');
ok($node->log_contains(qr/no SSL error reported/) == 0,
'error message translated');
-$node->append_conf('ssl_config.conf', qq{ssl_groups='prime256v1'});
+$node->append_conf('sslconfig.conf', qq{ssl_groups='prime256v1'});
$result = $node->restart(fail_ok => 1);
### Run client-side tests.
@@ -1004,4 +1004,43 @@ $node->connect_fails(
qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
+# Test client CAs
+my $connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+switch_server_cert($node, certfile => 'server-cn-only', cafile => '');
+# example.org is unconfigured and should fail.
+$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/);
+
+# example.com uses the 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'),
+ "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');
+# 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 "
+ . 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/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index 2dd70e7afee..348255e1602 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -66,8 +66,19 @@ $node->connect_fails(
"pg.conf: connect fails without intermediate for sslmode=verify-ca",
expected_stderr => qr/certificate verify failed/);
-# Remove pg_hosts.conf and reload to make sure a missing file is treated like
-# an empty file.
+# 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;
@@ -107,6 +118,10 @@ $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",
@@ -121,19 +136,50 @@ $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/);
+
+# 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'
+);
+
# Modify pg_hosts.conf to no longer have the default host entry.
ok(unlink($node->data_dir . '/pg_hosts.conf'));
$node->append_conf('pg_hosts.conf',
"example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
);
-$node->reload;
+$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/missing extension/);
+ 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",
@@ -145,7 +191,7 @@ 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);
+$result = $node->restart(fail_ok => 1);
is($result, 0,
'pg_hosts.conf: restart fails with password-protected key when using the wrong passphrase command'
);
@@ -179,16 +225,21 @@ $node->connect_ok(
);
# Test reloading a passphrase protected key without reloading support in the
-# passphrase hook. Connecting after restart should succeed but not after the
-# following reload.
+# 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
@@ -199,19 +250,26 @@ SKIP:
"$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');
}
-$node->reload;
-$node->connect_fails(
- "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
- "pg_hosts.conf: connect fails since the passphrase protected key cannot be reloaded"
-);
-
# Configure with only non-SNI connections allowed
ok(unlink($node->data_dir . '/pg_hosts.conf'));
$node->append_conf('pg_hosts.conf',
- "no_sni server-cn-only.crt server-cn-only.key");
-$node->reload;
+ "/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",
@@ -222,68 +280,54 @@ $node->connect_fails(
"pg_hosts.conf: only non-SNI connections allowed, connecting with SNI",
expected_stderr => qr/unrecognized name/);
-# Test client CAs by connecting to hosts in pg_hosts.conf while at the same
-# time swapping out default contexts containing different CA configurations.
+# 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.org server-cn-only.crt server-cn-only.key');
# example.com uses the client CA.
$node->append_conf('pg_hosts.conf',
'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
# example.net uses the server CA (which is wrong).
$node->append_conf('pg_hosts.conf',
'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
-$node->reload;
+$node->restart;
$connstr =
"user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
-foreach my $default_ca ("", "root+client_ca", "root+server_ca")
-{
- # The default CA should, not matter for the purposes of these tests, since
- # we connect to the other hosts explicitly. Test with various default CA
- # settings to ensure it's isolated from the actual connections.
- $ssl_server->switch_server_cert(
- $node,
- certfile => 'server-cn-only',
- cafile => $default_ca);
-
- # example.org is unconfigured and should fail.
- $node->connect_fails(
- "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt "
- . $ssl_server->sslkey('client.key'),
- "host: 'example.org', ca: '$default_ca': connect with sslcert, no client CA configured",
- expected_stderr => qr/unknown ca/);
-
- # example.com is configured and should require a valid client cert.
- $node->connect_fails(
- "$connstr host=example.com sslcertmode=disable",
- "host: 'example.com', ca: '$default_ca': connect fails if no client certificate sent",
- expected_stderr => qr/connection requires a valid client certificate/
- );
+# 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/);
- $node->connect_ok(
- "$connstr host=example.com sslrootcert=ssl/root+server_ca.crt sslcertmode=require sslcert=ssl/client.crt "
- . $ssl_server->sslkey('client.key'),
- "host: 'example.com', ca: '$default_ca': connect with sslcert, client certificate sent"
- );
+# example.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/);
- # example.net is configured and should require a client cert, but will
- # always fail verification.
- $node->connect_fails(
- "$connstr host=example.net sslcertmode=disable",
- "host: 'example.net', ca: '$default_ca': connect fails if no client certificate sent",
- expected_stderr => qr/connection requires a valid client certificate/
- );
+$node->connect_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"
+);
- $node->connect_fails(
- "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
- . $ssl_server->sslkey('client.key'),
- "host: 'example.net', ca: '$default_ca': connect with sslcert, client certificate sent",
- expected_stderr => qr/unknown ca/);
-}
+# 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();
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-01-16 23:44 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Jacob Champion @ 2026-01-16 23:44 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
[skipping right to the weird part, will circle back to the other
questions later]
On Tue, Jan 13, 2026 at 1:57 AM Daniel Gustafsson <[email protected]> wrote:
> I think the attached is pretty clear improvement over the previous version so
> thanks for the review suggestions. That being said, the test which was
> reported to still fail upstream is failing here as well (it does the right
> thing with the connection, but terminates the handshake in a different place).
> In an attempt to fix that I moved to using the ClientHello callback which
> OpenSSL document to be the right one (yet they use the servername callback
> themselves), but it renders the same result. I hope that your eagle eyes (or
> someone elses) can figure out either what is wrong, or if this is a different
> form of right. The same failing test is added to 0001 to run it in a strictly
> non-SNI config as well.
I hadn't realized that this also regressed without SNI! That helped a lot.
With 0001, the bug is this diff, which runs the verify_cb regardless
of the ssl_ca setting:
> - SSL_CTX_set_verify(context,
> - (SSL_VERIFY_PEER |
> - SSL_VERIFY_CLIENT_ONCE),
> - verify_cb);
> }
>
> + /*
> + * Always ask for SSL client cert, but don't fail if it's not presented.
> + * We might fail such connections later, depending on what we find in
> + * pg_hba.conf.
> + */
> + SSL_CTX_set_verify(context,
> + (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
> + verify_cb);
0002 undid that but reintroduced it in be_tls_open_server():
> /* enable ALPN */
> SSL_CTX_set_alpn_select_cb(SSL_context, alpn_cb, port);
>
> + SSL_CTX_set_verify(SSL_context,
> + (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
> + verify_cb);
If I remove that diff, the regression goes away. But then we start
failing SNI tests:
> [14:24:05.901](0.000s) not ok 72 - host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent: no stderr
> [14:24:05.902](0.000s) # Failed test 'host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent: no stderr'
> # at src/test/ssl/t/004_sni.pl line 314.
> [14:24:05.902](0.000s) # got: 'psql: error: connection to server at "127.0.0.1", port 15428 failed: FATAL: connection requires a valid client certificate'
> # expected: ''
> [14:24:05.917](0.000s) not ok 76 - host: 'example.net', ca: 'root+server_ca.crt': connect with sslcert, client certificate sent: matches
> [14:24:05.917](0.000s) # Failed test 'host: 'example.net', ca: 'root+server_ca.crt': connect with sslcert, client certificate sent: matches'
> # at ssl/t/004_sni.pl line 327.
> [14:24:05.917](0.000s) # 'psql: error: connection to server at "127.0.0.1", port 15428 failed: FATAL: connection requires a valid client certificate'
> # doesn't match '(?^:unknown ca)'
I think the root problem probably comes back to SSL_set_SSL_CTX [1].
That copies the certificate over from the new SSL_CTX, but it doesn't
really seem to care about much else, and there are a _lot_ of settings
copied into the SSL pointer during initial connection [2] that are
ignored there.
The verify mode and callback are two such settings. So is the password
callback (which may mean that the new per-host-line logic for
openssl_tls_init_hook won't work correctly either).
So unless Matt Caswell knows of an existing API that does this right,
I think I'm coming back to the idea that we should keep a single
SSL_CTX, and then use the selected HostsLine to override individual
connection settings during the clienthello/servername callback. Do we
give anything up with that approach?
--Jacob
[1] https://postgr.es/m/CAOYmi%2Bk%3DVF-2BCqfR49A92tx%3D_QNuL%3D3iT3w6FysOffKw9cxDQ%40mail.gmail.com
[2] https://github.com/openssl/openssl/blob/5d401004a0/ssl/ssl_lib.c#L731
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-06 22:11 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-06 22:11 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 17 Jan 2026, at 00:44, Jacob Champion <[email protected]> wrote:
> I think the root problem probably comes back to SSL_set_SSL_CTX [1].
> That copies the certificate over from the new SSL_CTX, but it doesn't
> really seem to care about much else, and there are a _lot_ of settings
> copied into the SSL pointer during initial connection [2] that are
> ignored there.
>
> The verify mode and callback are two such settings. So is the password
> callback (which may mean that the new per-host-line logic for
> openssl_tls_init_hook won't work correctly either).
>
> So unless Matt Caswell knows of an existing API that does this right,
> I think I'm coming back to the idea that we should keep a single
> SSL_CTX, and then use the selected HostsLine to override individual
> connection settings during the clienthello/servername callback. Do we
> give anything up with that approach?
After discussing this more off-list we collaborated on rewriting the mechanics
for switching out the SSL_CTX settings during SNI selection in the clienthello
callback. The attached version implements this modified approach.
The code now has a single main SSL_CTX object which is reconfigured rather than
swapped out. The HostsLine struct, which keeps the parsed pg_hosts.conf
information, gains an SSL_CTX object which contains the host specific settings,
and this is where they are then copied to the single main during
reconfiguration. The interface with Postgres and how SNI is configured has not
been changed at all. Users who don't enable ssl_sni and configure SSL in the
usual way in postgresql.conf will not notice any difference from today (and
ssl_sni is set to off by default).
As discussed above, the tls_init hook will not work very well for a multi host
setup so in the attached it will only be executed when ssl_sni is set to off.
When ssl_sni is on the ssl_passphrase_cmd parameter will still be honored for
handling passphrases.
We also realized that LibreSSL doesn't support a lot of the functionality
required, as it is *IMHO* falling further and further behind OpenSSL in it's
compatibility layer. The patch adds meson/autoconf checks for required API's
and require these to be present for ssl_sni to be enabled. Longer term I think
we need to start thinking about splitting be-secure-openssl.c into a
be-secure-libressl.c to keep the ifdef soup from getting too bad. Thats for
another patch however.
Some of the new tests added for this patchset turned out to be valuable on
their own as they fill a gap in coverage, they have been pulled out into 0001.
0002 has a few small TODO comments left but is feature complete.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v15-0002-ssl-Serverside-SNI-support-for-libpq.patch (79.9K, 2-v15-0002-ssl-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 4d5253b0cf1d0a981ee4ac1b750310c7c70197b5 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
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 <[email protected]>
Co-authored-by: Jacob Champion <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Dewei Dai <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
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)
[application/octet-stream] v15-0001-ssl-Add-tests-for-client-CA.patch (4.6K, 3-v15-0001-ssl-Add-tests-for-client-CA.patch)
download | inline diff:
From 55190cd28ed1498378b6709f927dd694bf86eb6f Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 6 Mar 2026 22:43:06 +0100
Subject: [PATCH v15 1/2] ssl: Add tests for client CA
These tests were originally written to test the SSL SNI patchset
but they have merit on their own since we lack coverage for these
scenarios in the non SNI case as well.
Author: Jacob Champion <[email protected]>
Co-authored-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/test/ssl/t/001_ssltests.pl | 39 +++++++++++++++++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 ++++++++---
2 files changed, 52 insertions(+), 3 deletions(-)
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 2b9b3dfd663..a86e8ff0e86 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1004,4 +1004,43 @@ $node->connect_fails(
qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
+# Test client CAs
+my $connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+switch_server_cert($node, certfile => 'server-cn-only', cafile => '');
+# example.org is unconfigured and should fail.
+$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/);
+
+# example.com uses the 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'),
+ "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');
+# 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 "
+ . 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/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 7ea05572a8d..6060771c1a8 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-10 13:11 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-10 13:11 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
The attached rebase adds a check, and testcase, for duplicated hostname entries
in pg_hosts.conf and errors out in case a host is configured multiple times.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v16-0001-ssl-Add-tests-for-client-CA.patch (4.6K, 2-v16-0001-ssl-Add-tests-for-client-CA.patch)
download | inline diff:
From 5335afe9bd6de3517ce740e94385d4a08a521ab4 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 6 Mar 2026 22:43:06 +0100
Subject: [PATCH v16 1/2] ssl: Add tests for client CA
These tests were originally written to test the SSL SNI patchset
but they have merit on their own since we lack coverage for these
scenarios in the non SNI case as well.
Author: Jacob Champion <[email protected]>
Co-authored-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/test/ssl/t/001_ssltests.pl | 39 +++++++++++++++++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 ++++++++---
2 files changed, 52 insertions(+), 3 deletions(-)
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 2b9b3dfd663..a86e8ff0e86 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1004,4 +1004,43 @@ $node->connect_fails(
qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
+# Test client CAs
+my $connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+switch_server_cert($node, certfile => 'server-cn-only', cafile => '');
+# example.org is unconfigured and should fail.
+$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/);
+
+# example.com uses the 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'),
+ "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');
+# 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 "
+ . 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/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 7ea05572a8d..6060771c1a8 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
--
2.39.3 (Apple Git-146)
[application/octet-stream] v16-0002-ssl-Serverside-SNI-support-for-libpq.patch (82.8K, 3-v16-0002-ssl-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From ce37f9d823fa61a0415a441b6243f287b7dac0b6 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 6 Mar 2026 23:00:51 +0100
Subject: [PATCH v16 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 <[email protected]>
Co-authored-by: Jacob Champion <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Dewei Dai <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
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 | 846 ++++++++++++++++--
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 | 412 +++++++++
src/tools/pgindent/typedefs.list | 2 +
26 files changed, 1740 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 42621ecd051..f1d2a802bcb 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 61ec895d23c..dd57e087da8 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..c9391a1e714 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -27,6 +27,7 @@
#include <netinet/tcp.h>
#include <arpa/inet.h>
+#include "common/hashfn.h"
#include "common/string.h"
#include "libpq/libpq.h"
#include "miscadmin.h"
@@ -52,6 +53,27 @@
#endif
#include <openssl/x509v3.h>
+/*
+ * Simplehash for tracking configured hostnames to guard against duplicate
+ * entries. Each list of hosts is traversed and added to the hash during
+ * parsing and if a duplicate error is detected an error will be thrown.
+ */
+typedef struct
+{
+ uint32 status;
+ const char *hostname;
+} HostCacheEntry;
+static uint32 host_cache_pointer(const char *key);
+#define SH_PREFIX host_cache
+#define SH_ELEMENT_TYPE HostCacheEntry
+#define SH_KEY_TYPE const char *
+#define SH_KEY hostname
+#define SH_HASH_KEY(tb, key) host_cache_pointer(key)
+#define SH_EQUAL(tb, a, b) (pg_strcasecmp(a, b) == 0)
+#define SH_SCOPE static inline
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
/* default init hook can be overridden by a shared library */
static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
@@ -78,10 +100,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 +150,269 @@ 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;
+ host_cache_hash *host_cache = NULL;
/*
- * 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
+ {
+ /* Check the hostnames for duplicates */
+ if (!host_cache)
+ host_cache = host_cache_create(host_memcxt, 32, NULL);
+
+ foreach_ptr(char, hostname, host->hostnames)
+ {
+ HostCacheEntry *entry;
+ bool found;
+
+ entry = host_cache_insert(host_cache, hostname, &found);
+ if (found)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple entries for host \"%s\" specified",
+ hostname),
+ errcontext("line %d of configuration file \"%s\"",
+ host->linenumber, host->sourcefile));
+ goto error;
+ }
+ else
+ entry->hostname = pstrdup(hostname);
+ }
+
+ /*
+ * At this point we know we have a configuration with a list
+ * of distnct 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 +550,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 +740,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 +750,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 +791,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 +853,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 +1541,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 +1791,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
@@ -1791,6 +2443,20 @@ ssl_protocol_version_to_string(int v)
return "(unrecognized)";
}
+static uint32
+host_cache_pointer(const char *key)
+{
+ uint32 hash;
+ char *lkey = pstrdup(key);
+ int len = strlen(key);
+
+ for (int i = 0; i < len; i++)
+ lkey[i] = pg_tolower(lkey[i]);
+
+ hash = string_hash((const void *) lkey, len);
+ pfree(lkey);
+ return hash;
+}
static void
default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
@@ -1798,12 +2464,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..6fe93fc1607
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,412 @@
+
+# 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');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org server-cn-only.crt server-cn-only.key
+example.net server-cn-only.crt server-cn-only.key
+example.org server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two identical hostname entries');
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org server-cn-only.crt server-cn-only.key
+example.net,example.com,Example.org server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two identical hostname entries in lists');
+
+# 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)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-12 14:36 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-12 14:36 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 10 Mar 2026, at 14:11, Daniel Gustafsson <[email protected]> wrote:
>
> The attached rebase adds a check, and testcase, for duplicated hostname entries
> in pg_hosts.conf and errors out in case a host is configured multiple times.
And another small update to SKIP the newly added tests on LibreSSL since they
use sslmode require which is only available in OpenSSL. No other changes to
the patchset in this version (apart from a freshly brewed rebase of course).
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v17-0002-ssl-Serverside-SNI-support-for-libpq.patch (81.2K, 2-v17-0002-ssl-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 477cba1c0dfcf697d4e598f20fbe0e748a4f4942 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 6 Mar 2026 23:00:51 +0100
Subject: [PATCH v17 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 <[email protected]>
Co-authored-by: Jacob Champion <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Dewei Dai <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
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 | 846 ++++++++++++++++--
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 | 6 +-
src/test/ssl/t/004_sni.pl | 412 +++++++++
src/tools/pgindent/typedefs.list | 2 +
26 files changed, 1727 insertions(+), 98 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 42621ecd051..f1d2a802bcb 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 61ec895d23c..dd57e087da8 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..c9391a1e714 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -27,6 +27,7 @@
#include <netinet/tcp.h>
#include <arpa/inet.h>
+#include "common/hashfn.h"
#include "common/string.h"
#include "libpq/libpq.h"
#include "miscadmin.h"
@@ -52,6 +53,27 @@
#endif
#include <openssl/x509v3.h>
+/*
+ * Simplehash for tracking configured hostnames to guard against duplicate
+ * entries. Each list of hosts is traversed and added to the hash during
+ * parsing and if a duplicate error is detected an error will be thrown.
+ */
+typedef struct
+{
+ uint32 status;
+ const char *hostname;
+} HostCacheEntry;
+static uint32 host_cache_pointer(const char *key);
+#define SH_PREFIX host_cache
+#define SH_ELEMENT_TYPE HostCacheEntry
+#define SH_KEY_TYPE const char *
+#define SH_KEY hostname
+#define SH_HASH_KEY(tb, key) host_cache_pointer(key)
+#define SH_EQUAL(tb, a, b) (pg_strcasecmp(a, b) == 0)
+#define SH_SCOPE static inline
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
/* default init hook can be overridden by a shared library */
static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
@@ -78,10 +100,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 +150,269 @@ 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;
+ host_cache_hash *host_cache = NULL;
/*
- * 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
+ {
+ /* Check the hostnames for duplicates */
+ if (!host_cache)
+ host_cache = host_cache_create(host_memcxt, 32, NULL);
+
+ foreach_ptr(char, hostname, host->hostnames)
+ {
+ HostCacheEntry *entry;
+ bool found;
+
+ entry = host_cache_insert(host_cache, hostname, &found);
+ if (found)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple entries for host \"%s\" specified",
+ hostname),
+ errcontext("line %d of configuration file \"%s\"",
+ host->linenumber, host->sourcefile));
+ goto error;
+ }
+ else
+ entry->hostname = pstrdup(hostname);
+ }
+
+ /*
+ * At this point we know we have a configuration with a list
+ * of distnct 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 +550,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 +740,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 +750,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 +791,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 +853,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 +1541,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 +1791,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
@@ -1791,6 +2443,20 @@ ssl_protocol_version_to_string(int v)
return "(unrecognized)";
}
+static uint32
+host_cache_pointer(const char *key)
+{
+ uint32 hash;
+ char *lkey = pstrdup(key);
+ int len = strlen(key);
+
+ for (int i = 0; i < len; i++)
+ lkey[i] = pg_tolower(lkey[i]);
+
+ hash = string_hash((const void *) lkey, len);
+ pfree(lkey);
+ return hash;
+}
static void
default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
@@ -1798,12 +2464,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 a5a0edf2534..bf092b4cdeb 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 963bfea8ed5..0af887caa63 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -380,11 +380,11 @@ switch_server_cert($node, certfile => 'server-ip-cn-only');
$common_connstr =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in the Common Name");
$node->connect_fails(
- "$common_connstr host=192.000.002.001",
+ "$common_connstr host=192.000.002.001 sslsni=0",
"mismatch between host name and server certificate IP address",
expected_stderr =>
qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
@@ -394,7 +394,7 @@ $node->connect_fails(
# long-standing behavior.)
switch_server_cert($node, certfile => 'server-ip-in-dnsname');
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in a dNSName");
# Test Subject Alternative Names.
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..6fe93fc1607
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,412 @@
+
+# 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');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org server-cn-only.crt server-cn-only.key
+example.net server-cn-only.crt server-cn-only.key
+example.org server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two identical hostname entries');
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org server-cn-only.crt server-cn-only.key
+example.net,example.com,Example.org server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two identical hostname entries in lists');
+
+# 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 141b9d6e077..ba2bad0c35a 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)
[application/octet-stream] v17-0001-ssl-Add-tests-for-client-CA.patch (4.8K, 3-v17-0001-ssl-Add-tests-for-client-CA.patch)
download | inline diff:
From 3a1edcb7ed8ec151b5fc3a22d2a443f6e33c8560 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 6 Mar 2026 22:43:06 +0100
Subject: [PATCH v17 1/2] ssl: Add tests for client CA
These tests were originally written to test the SSL SNI patchset
but they have merit on their own since we lack coverage for these
scenarios in the non SNI case as well.
Author: Jacob Champion <[email protected]>
Co-authored-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/test/ssl/t/001_ssltests.pl | 56 +++++++++++++++++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 ++++++--
2 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 2b9b3dfd663..963bfea8ed5 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1004,4 +1004,60 @@ $node->connect_fails(
qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
+SKIP:
+{
+ skip "sslmode require not supported in this build", 4
+ unless ($supports_sslcertmode_require);
+
+ # Test client CAs
+ my $connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+ switch_server_cert($node, certfile => 'server-cn-only', cafile => '');
+ # example.org is unconfigured and should fail.
+ $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/
+ );
+
+ # example.com uses the 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'),
+ "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');
+ # 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 "
+ . 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/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 7ea05572a8d..6060771c1a8 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
chmod(0600, glob "$pgdata/server-*.key")
or die "failed to change permissions on server keys: $!";
_copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root+server_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
=item cafile => B<value>
The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
=item certfile => B<value>
@@ -181,10 +183,18 @@ sub set_server_cert
unless defined $params->{keyfile};
my $sslconf =
- "ssl_ca_file='$params->{cafile}.crt'\n"
- . "ssl_cert_file='$params->{certfile}.crt'\n"
+ "ssl_cert_file='$params->{certfile}.crt'\n"
. "ssl_key_file='$params->{keyfile}.key'\n"
. "ssl_crl_file='$params->{crlfile}'\n";
+ if ($params->{cafile} ne "")
+ {
+ $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+ }
+ else
+ {
+ $sslconf .= "ssl_ca_file=''\n";
+ }
+
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-13 21:12 Zsolt Parragi <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Zsolt Parragi @ 2026-03-13 21:12 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Hello
Originally I started looking at this thread because Jacob mentioned it
relates to the custom OAuth / HBA variable question[1]. While I do see
the relation, I don't have many practical ideas.
I was thinking about suggesting using a "key=value, key=value ..."
file style instead of the fixed table, both for easier later
generalization, and because it aligns better to modern configuration
formats (and personally I always find these columnar config files
harder to read)
However, it would also differ from other existing postgres config
files and wouldn't offer a clear initial advantage, so it doesn't seem
like a good practical choice. I'm still mentioning this for
completeness, but mostly I'll focus on a more practical review:
+ check_nook => 'check_ssl_sni',
This seems to be a typo?
+ if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &tlsext, &left))
+ {
+ if (left <= 2)
+ {
+ *al = SSL_AD_MISSING_EXTENSION;
+ return 0;
+ }
... and later error returns in this if block seem to use the wrong
error code to me: truncated length, length mismatch, empty list,
length exceeding remaining data...
missing_extension: Sent by endpoints that receive a handshake
message not containing an extension that is mandatory to send for
the offered TLS version or other negotiated parameters.
decode_error: A message could not be decoded because some field was
out of the specified range or the length of the message was
incorrect. This alert is used for errors where the message does
not conform to the formal protocol syntax. This alert should
never be observed in communication between proper implementations,
except when messages were corrupted in the network.
Since we are already inside the if which verifies that the extension
is present, shouldn't all of these report decode_error?
+ 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;
+ }
Isn't there a missing *al = assignment here?
+ /*
+ * 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;
+ }
The comment suggests that this aims to prevent any additional text on
the line, but this parses:
localhost server.crt server.key server.crt "cmd" on TRAILING_TEXT MORE_TEXT
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
Isn't a length > 1 error check missing from here?
[1]: https://www.postgresql.org/message-id/CAN4CZFM3b8u5uNNNsY6XCya257u%2BDofms3su9f11iMCxvCacag%40mail.g...
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-17 06:22 Zsolt Parragi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Zsolt Parragi @ 2026-03-17 06:22 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> I'm a bit surprised that the .dat
> file processing doesn't error out keys that aren't part of the DSL for defining
> GUCs but clearly it doesn't. Fixed.
This also surprised me, I wrote a patch to improve this [1].
I only have a few mostly stylistic comments, otherwise the patch looks good.
+ 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);
+ }
The start path checks ssl_passphrase_cmd for null, the reload doesn't.
+ if (openssl_tls_init_hook != default_openssl_tls_init)
+ {
+ ereport(WARNING,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("SNI is enabled; installed TLS init hook will be ignored"),
Won't this spam the log with one warning per hosts line? It might be
okay/acceptable, but there isn't anything line specific in this
warning.
+ * 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)
Comment says HostsFileLoadResult, but the return type is int.
+typedef enum HostsFileLoad
+{
+ HOSTSFILE_LOAD_OK = 0,
+ HOSTSFILE_LOAD_FAILED,
+ HOSTSFILE_EMPTY,
+ HOSTSFILE_MISSING,
+ HOSTSFILE_DISABLED,
+} HostsFileLoadResult;
Is the HostsFileLoad vs HostsFileLoadResult difference intentional?
+#ifdef USE_ASSERT_CHECKING
+ if (res == HOSTSFILE_DISABLED)
+ Assert(ssl_sni == false);
+#endif
Do we need this ifdef?
And I also found a typo (distncnt):
+ /*
+ * At this point we know we have a configuration with a list
+ * of distnct 1..n hostnames for literal string matching with
+ * the SNI extension from the user.
[1]: https://www.postgresql.org/message-id/CAN4CZFP%3D3xUoXb9jpn5OWwicg%2Brbyrca8-tVmgJsQAa4%2BOExkw%40ma...
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 12:19 Daniel Gustafsson <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-18 12:19 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
After staring at this version more, and testing on various platforms with
various OpenSSL versions, I went ahead and pushed it. Thanks for all the
reviews!
longfin has so far reported a test failure which I am looking into.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 13:01 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Jacob Champion @ 2026-03-18 13:01 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, Mar 18, 2026 at 5:19 AM Daniel Gustafsson <[email protected]> wrote:
> longfin has so far reported a test failure which I am looking into.
I took a quick look at culicidae and I think that's just due to the
use of EXEC_BACKEND. Rather than $windows_os the SKIP logic should
probably use something like 001_server's $exec_backend.
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 14:25 Daniel Gustafsson <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-18 14:25 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 18 Mar 2026, at 14:01, Jacob Champion <[email protected]> wrote:
>
> On Wed, Mar 18, 2026 at 5:19 AM Daniel Gustafsson <[email protected]> wrote:
>> longfin has so far reported a test failure which I am looking into.
>
> I took a quick look at culicidae and I think that's just due to the
> use of EXEC_BACKEND. Rather than $windows_os the SKIP logic should
> probably use something like 001_server's $exec_backend.
That's a bit embarrassing, I spent some time investigating passphrase reloading
under EXEC_BACKEND as part of this patchset..
The longfin issue is a bit more odd, I can reproduce it on macOS with OpenSSL
1.1.1 but nowhere else. Rather than reporting an SSL error for aborted
handshake it reports a SYSCALL error. Using SYSCALL error for when the server
close the connection abruptly is documented, but not really this case where it
does so with no error codes at all (which given OpenSSL documentation doesn't
really say much..). The change in the attached diff does fix it for me but I'm
a bit hesitant to apply something like that, I would be more inclined to the
change the expected output in the test. What are your thoughts?
--
Daniel Gustafsson
Attachments:
[application/octet-stream] bf_fixes.diff (1.6K, 2-bf_fixes.diff)
download | inline diff:
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index fbd3c63fb5d..943dd2d6767 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -1381,6 +1381,8 @@ open_client_SSL(PGconn *conn)
else if (r == -1 && save_errno != 0)
libpq_append_conn_error(conn, "SSL SYSCALL error: %s",
SOCK_STRERROR(save_errno, sebuf, sizeof(sebuf)));
+ else if (save_errno == 0 && vcode == X509_V_OK && ecode == 0)
+ libpq_append_conn_error(conn, "SSL error: handshake failure");
else
libpq_append_conn_error(conn, "SSL SYSCALL error: EOF detected");
pgtls_close(conn);
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index 4e06475b125..878e32ff107 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -47,6 +47,9 @@ $ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
+my $exec_backend = $node->safe_psql('postgres', 'SHOW debug_exec_backend');
+chomp($exec_backend);
+
$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
$SERVERHOSTCIDR, 'trust');
@@ -320,9 +323,10 @@ unlike(
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);
+ # Passphrase reloads must be enabled on Windows (and EXEC_BACKEND) to
+ # succeed even without a restart
+ skip "Passphrase command reload required on Windows", 1
+ if ($windows_os || $exec_backend =~ /on/);
$node->connect_ok(
"$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 14:58 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-18 14:58 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Jacob Champion <[email protected]>; Zsolt Parragi <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 18 Mar 2026, at 15:33, Tom Lane <[email protected]> wrote:
>
> Daniel Gustafsson <[email protected]> writes:
>> The longfin issue is a bit more odd, I can reproduce it on macOS with OpenSSL
>> 1.1.1 but nowhere else. Rather than reporting an SSL error for aborted
>> handshake it reports a SYSCALL error.
>
> IIRC longfin is using some fairly old hand-built openssl installation.
Thanks for confirming, that matches with my local repro which required building
1.1.1 on macOS. Did you build with Apple clang or a a stock clang/gcc?
> Maybe I should just update it. Do you want to hold off and see if
> that changes anything?
Let's wait a little while I research little more. I can reproduce it locally
but it's better with two independent cases.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 15:35 Tom Lane <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Tom Lane @ 2026-03-18 15:35 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Zsolt Parragi <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Daniel Gustafsson <[email protected]> writes:
> On 18 Mar 2026, at 15:33, Tom Lane <[email protected]> wrote:
>> IIRC longfin is using some fairly old hand-built openssl installation.
> Thanks for confirming, that matches with my local repro which required building
> 1.1.1 on macOS. Did you build with Apple clang or a a stock clang/gcc?
It's been awhile, but I can't imagine that I didn't use Apple's
compiler of the time. [ Checks longfin's host... ] The files
in that openssl tree are all dated Nov 20 2018, so it's probably
due for a refresh in any case.
regards, tom lane
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 16:14 Jacob Champion <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Jacob Champion @ 2026-03-18 16:14 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
On Wed, Mar 18, 2026 at 7:25 AM Daniel Gustafsson <[email protected]> wrote:
> The longfin issue is a bit more odd, I can reproduce it on macOS with OpenSSL
> 1.1.1 but nowhere else. Rather than reporting an SSL error for aborted
> handshake it reports a SYSCALL error.
Do you know yet why the handshake is aborted on macOS, as opposed to a
polite handshake_failure alert?
> The change in the attached diff does fix it for me but I'm
> a bit hesitant to apply something like that, I would be more inclined to the
> change the expected output in the test. What are your thoughts?
I think that patch might effectively shadow the `else` branch, which
is supposed to be reporting the EOF. (I wouldn't mind a better error
message than "SYSCALL error: EOF detected", but that's not something
this patch did.)
--Jacob
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-18 18:39 Tom Lane <[email protected]>
parent: Tom Lane <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Tom Lane @ 2026-03-18 18:39 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Jacob Champion <[email protected]>; Zsolt Parragi <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Daniel Gustafsson <[email protected]> writes:
> I've done more testing now and I can only reproduce this with downgraded
> versions of OpenSSL 1.1.1, when running the latest 1.1.1x the error goes away
> and the error is reported as expected. Can you try to upgrade your machine,
> which I assume isn't running bleeding edge 1.1.1 by the sounds of it.
Nope, it was still on 1.1.1a. I've now updated it to 3.0.19,
which appears to be the oldest available-to-the-public supported
version. I doubt that continuing to test 1.1.1-anything is really
useful.
... and longfin is now green.
regards, tom lane
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-03-19 09:45 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-03-19 09:45 UTC (permalink / raw)
To: Michael Banck <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; Dewei Dai <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 19 Mar 2026, at 10:39, Michael Banck <[email protected]> wrote:
> I'm really late to the part, but I did not see it discussed elsewhere on
> a quick glance: Isn't pg_hosts.conf a really (too) generic name for this
> feature? I don't want to open a huge bikeshedding sub-thread, but was a
> more specific filename considered?
I don't recall any discussion on that, and I don't really see a problem off the
cuff. As it is a config file for defining hostnames and their config, in which
way do you feel its too generic and what would "claiming that name" for this
prevent (or how would it confuse)? Do you have any alternative suggestions?
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-05-04 19:22 Tom Lane <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Tom Lane @ 2026-05-04 19:22 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Michael Banck <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
In preparation for our annual pgindent update, I checked what happens
when I install the buildfarm's version of typedefs.list, and I found
that the typedef HostsFileLoadResult (from 4f433025f) gets
misformatted because it's not in the buildfarm's list. That's because
the buildfarm mechanism only captures typedefs that are used to
declare some object (variable, function, field) and this one isn't.
It seems quite odd to me that load_host(), which in fact returns
HostsFileLoadResult codes, is declared to return int. That seems
to have been done because HostsFileLoadResult wasn't declared in
the same header, but there is no visible reason why it shouldn't be.
Any objection to the attached fixup?
As a side matter, "load_host" seems like a remarkably generic name
that conveys little about what it actually does, and to the extent
that it does convey anything the implication is wrong: it returns
(potentially) info about multiple hosts not just one. Can't we do
better?
regards, tom lane
Attachments:
[text/x-diff] declare-load_host-more-honestly.patch (2.4K, 2-declare-load_host-more-honestly.patch)
download | inline diff:
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index ad04bedaa1d..6ec887b8a47 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -361,7 +361,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
* the hosts configuration failed to load, the err_msg variable may have more
* information in case it was passed as non-NULL.
*/
-int
+HostsFileLoadResult
load_hosts(List **hosts, char **err_msg)
{
FILE *file;
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index f64b2787f66..b978497b5d4 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -156,7 +156,7 @@ be_tls_init(bool isServerStart)
MemoryContext host_memcxt = NULL;
MemoryContextCallback *host_memcxt_cb;
char *err_msg = NULL;
- int res;
+ HostsFileLoadResult res;
struct hosts *new_hosts;
SSL_CTX *context = NULL;
int ssl_ver_min = -1;
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 29e2a6c5b3d..4aa6258a345 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -165,15 +165,6 @@ typedef struct HostsLine
void *ssl_ctx; /* associated SSL_CTX* for the above settings */
} HostsLine;
-typedef enum HostsFileLoadResult
-{
- 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 c9b934d2321..d15073a0a93 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -156,6 +156,15 @@ enum ssl_protocol_versions
PG_TLS1_3_VERSION,
};
+typedef enum HostsFileLoadResult
+{
+ HOSTSFILE_LOAD_OK = 0,
+ HOSTSFILE_LOAD_FAILED,
+ HOSTSFILE_EMPTY,
+ HOSTSFILE_MISSING,
+ HOSTSFILE_DISABLED,
+} HostsFileLoadResult;
+
/*
* prototypes for functions in be-secure-common.c
*/
@@ -164,6 +173,6 @@ extern int run_ssl_passphrase_command(const char *cmd, const char *prompt,
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);
+extern HostsFileLoadResult load_hosts(List **hosts, char **err_msg);
#endif /* LIBPQ_H */
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-05-04 19:27 Tom Lane <[email protected]>
parent: Tom Lane <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Tom Lane @ 2026-05-04 19:27 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Michael Banck <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
I wrote:
> As a side matter, "load_host" seems like a remarkably generic name
> that conveys little about what it actually does, and to the extent
> that it does convey anything the implication is wrong: it returns
> (potentially) info about multiple hosts not just one. Can't we do
> better?
Sigh ... brain fade there, of course the function is load_hosts
not load_host. It's still too generic IMO, but at least the
pluralization is right.
regards, tom lane
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-05-04 20:31 Daniel Gustafsson <[email protected]>
parent: Tom Lane <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Daniel Gustafsson @ 2026-05-04 20:31 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Michael Banck <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 4 May 2026, at 21:22, Tom Lane <[email protected]> wrote:
> It seems quite odd to me that load_host(), which in fact returns
> HostsFileLoadResult codes, is declared to return int. That seems
> to have been done because HostsFileLoadResult wasn't declared in
> the same header, but there is no visible reason why it shouldn't be.
> Any objection to the attached fixup?
At some point during the development of the patch there was a reason (which I
cannot remember right now) for the declaration being in hba.h, but I clearly
missed moving it when that no longrer applied. No objections to the patch,
thanks!
> As a side matter, "load_host" seems like a remarkably generic name
> that conveys little about what it actually does, and to the extent
> that it does convey anything the implication is wrong: it returns
> (potentially) info about multiple hosts not just one. Can't we do
> better?
It's following the naming convention of load_hba() which reads pg_hba.conf, and
load_ident() which reads pg_ident.conf - thus load_hosts() for the function
that reads pg_hosts.conf. Perhaps load_pg_hosts_conf() or load_hosts_config()
would convey more meaning?
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-05-04 22:00 Tom Lane <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Tom Lane @ 2026-05-04 22:00 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Michael Banck <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
Daniel Gustafsson <[email protected]> writes:
>> On 4 May 2026, at 21:22, Tom Lane <[email protected]> wrote:
>> As a side matter, "load_host" seems like a remarkably generic name
>> that conveys little about what it actually does,
> It's following the naming convention of load_hba() which reads pg_hba.conf, and
> load_ident() which reads pg_ident.conf - thus load_hosts() for the function
> that reads pg_hosts.conf. Perhaps load_pg_hosts_conf() or load_hosts_config()
> would convey more meaning?
Hmm, okay. I'd prefer a more specific name, but it wouldn't make
much sense unless we also rename those two. That's probably more
code churn than is justified.
I'll push the thing for moving/using the typedef, but leave the
function name alone.
regards, tom lane
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: Serverside SNI support in libpq
@ 2026-05-05 07:40 Daniel Gustafsson <[email protected]>
parent: Tom Lane <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Daniel Gustafsson @ 2026-05-05 07:40 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; Michael Banck <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Heikki Linnakangas <[email protected]>; li.evan.chao <[email protected]>; Michael Paquier <[email protected]>; Andres Freund <[email protected]>; Pgsql Hackers <[email protected]>
> On 5 May 2026, at 00:00, Tom Lane <[email protected]> wrote:
> I'll push the thing for moving/using the typedef, but leave the
> function name alone.
Thanks!
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 58+ messages in thread
end of thread, other threads:[~2026-05-05 07:40 UTC | newest]
Thread overview: 58+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-12-03 13:58 Re: Serverside SNI support in libpq Daniel Gustafsson <[email protected]>
2024-12-04 00:43 ` Jacob Champion <[email protected]>
2024-12-04 13:44 ` Daniel Gustafsson <[email protected]>
2024-12-11 00:34 ` Michael Paquier <[email protected]>
2024-12-11 08:13 ` Daniel Gustafsson <[email protected]>
2025-02-19 23:12 ` Daniel Gustafsson <[email protected]>
2025-02-24 21:51 ` Jacob Champion <[email protected]>
2025-02-27 13:38 ` Daniel Gustafsson <[email protected]>
2025-03-04 21:57 ` Jacob Champion <[email protected]>
2025-05-13 13:46 ` Andres Freund <[email protected]>
2025-08-27 19:49 ` Daniel Gustafsson <[email protected]>
2025-09-01 01:58 ` Michael Paquier <[email protected]>
2025-09-02 12:48 ` Daniel Gustafsson <[email protected]>
2025-11-10 22:32 ` Daniel Gustafsson <[email protected]>
2025-11-11 09:06 ` Chao Li <[email protected]>
2025-11-12 22:50 ` Jacob Champion <[email protected]>
2025-11-12 22:44 ` Jacob Champion <[email protected]>
2025-11-12 23:03 ` Daniel Gustafsson <[email protected]>
2025-11-24 14:53 ` Daniel Gustafsson <[email protected]>
2025-11-24 23:28 ` Chao Li <[email protected]>
2025-11-25 14:39 ` Daniel Gustafsson <[email protected]>
2025-11-26 09:14 ` Dewei Dai <[email protected]>
2025-11-26 14:33 ` Daniel Gustafsson <[email protected]>
2025-12-03 09:57 ` Heikki Linnakangas <[email protected]>
2025-12-03 16:52 ` Daniel Gustafsson <[email protected]>
2025-12-03 16:56 ` Heikki Linnakangas <[email protected]>
2025-12-03 21:27 ` Jelte Fennema-Nio <[email protected]>
2025-12-03 23:27 ` Daniel Gustafsson <[email protected]>
2025-12-11 17:47 ` Jacob Champion <[email protected]>
2025-12-11 17:51 ` Daniel Gustafsson <[email protected]>
2025-12-11 19:40 ` Jacob Champion <[email protected]>
2025-12-12 11:41 ` Daniel Gustafsson <[email protected]>
2025-12-17 09:03 ` Heikki Linnakangas <[email protected]>
2025-12-17 09:06 ` Heikki Linnakangas <[email protected]>
2025-12-17 23:58 ` Jacob Champion <[email protected]>
2025-12-18 00:07 ` Daniel Gustafsson <[email protected]>
2025-12-18 17:06 ` Jacob Champion <[email protected]>
2025-12-18 18:20 ` Jacob Champion <[email protected]>
2026-01-13 09:57 ` Daniel Gustafsson <[email protected]>
2026-01-16 23:44 ` Jacob Champion <[email protected]>
2026-03-06 22:11 ` Daniel Gustafsson <[email protected]>
2026-03-10 13:11 ` Daniel Gustafsson <[email protected]>
2026-03-12 14:36 ` Daniel Gustafsson <[email protected]>
2026-03-13 21:12 ` Zsolt Parragi <[email protected]>
2026-03-17 06:22 ` Zsolt Parragi <[email protected]>
2026-03-18 12:19 ` Daniel Gustafsson <[email protected]>
2026-03-18 13:01 ` Jacob Champion <[email protected]>
2026-03-18 14:25 ` Daniel Gustafsson <[email protected]>
2026-03-18 14:58 ` Daniel Gustafsson <[email protected]>
2026-03-18 15:35 ` Tom Lane <[email protected]>
2026-03-18 18:39 ` Tom Lane <[email protected]>
2026-03-18 16:14 ` Jacob Champion <[email protected]>
2026-03-19 09:45 ` Daniel Gustafsson <[email protected]>
2026-05-04 19:22 ` Tom Lane <[email protected]>
2026-05-04 19:27 ` Tom Lane <[email protected]>
2026-05-04 20:31 ` Daniel Gustafsson <[email protected]>
2026-05-04 22:00 ` Tom Lane <[email protected]>
2026-05-05 07:40 ` Daniel Gustafsson <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox