From 7d36bcc4617f5c4ccfae50290a768b63fba796bb Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Fri, 26 Apr 2024 11:35:47 +0200 Subject: [PATCH v1] POC: serverside SNI support for libpq Experimental support for serverside SNI support in libpq, a new config file $datadir/pg_hosts.conf is used for configuring which certicate and key should be used for which hostname. A new GUC, ssl_snimode, is added which controls how the hostname TLS extension is handled. The possible values are off, default and strict: - off: pg_hosts.conf is not parsed and the hostname TLS extension is not inspected at all. The normal SSL GUCs for certificates and keys are used. - default: pg_hosts.conf is loaded as well as the normal GUCs. If no match for the TLS extension hostname is found in pg_hosts the cert and key from the postgresql.conf GUCs is used as the default (used as a wildcard host). - strict: only pg_hosts.conf is loaded and the TLS extension hostname MUST be passed and MUST have a match in the configuration, else the connection is refused. CRL file(s) are applied from postgresql.conf to all configured hostnames. --- doc/src/sgml/config.sgml | 62 +++++ doc/src/sgml/runtime.sgml | 40 +++ src/backend/Makefile | 1 + src/backend/libpq/be-secure-common.c | 156 +++++++++++ src/backend/libpq/be-secure-openssl.c | 253 +++++++++++++++--- src/backend/libpq/be-secure.c | 8 +- src/backend/libpq/meson.build | 1 + src/backend/libpq/pg_hosts.conf.sample | 5 + src/backend/utils/misc/guc.c | 26 ++ src/backend/utils/misc/guc_tables.c | 31 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/bin/initdb/initdb.c | 16 +- src/include/libpq/hba.h | 14 + src/include/libpq/libpq-be.h | 1 + src/include/libpq/libpq.h | 9 + src/include/utils/guc.h | 1 + src/test/ssl/meson.build | 1 + src/test/ssl/t/004_sni.pl | 86 ++++++ src/tools/pgindent/typedefs.list | 2 + 19 files changed, 673 insertions(+), 41 deletions(-) create mode 100644 src/backend/libpq/pg_hosts.conf.sample create mode 100644 src/test/ssl/t/004_sni.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e93208b2e6..6812173545 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1610,6 +1610,68 @@ include_dir 'conf.d' + + + ssl_snimode (enum) + + ssl_snimode configuration parameter + + + + + This parameter determines if the server will inspect the SNI TLS extension + when establishing the connection, and how it should be interpreted. + Valid values are currently: off, default and strict. + + + + + off + + + SNI is not enabled and no configuration from + pg_hosts.conf is loaded. Configuration of SSL + for all connections is done with , + and . + + + + + + default + + + SNI is enabled and hostname configuration is loaded from + pg_hosts.conf. , + and + are loaded as the default configuration. Any connection specifying + to 1 + a hostname which is missing in pg_hosts.conf + will be attempted using the default configuration. If the hostname + matches an entry from pg_hosts.conf, then the + configuration from that entry will be used for setting up the + connection. + + + + + + strict + + + SNI is enabled and all connections are required to set to 1 and + specify a hostname matching an entry in + pg_hosts.conf. Any connection without or with a hostname missing from + pg_hosts.conf will be rejected. + + + + + + + diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 6047b8171d..abe3a18fa5 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -2425,6 +2425,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433 client certificate must not be on this list + + $PGDATA/pg_hosts.conf + SNI configuration + defines which certificates to use for which server hostname + + @@ -2552,6 +2558,40 @@ openssl x509 -req -in server.csr -text -days 365 \ + + SNI Configuration + + + PostgreSQL can be configured for + SNI using the pg_hosts.conf + configuration file. PostgreSQL inspects the TLS + hostname extension in the SSL connection handshake, and selects the right + SSL certificate, key and CA certificate to use for the connection. + + + + SNI configuration is defined in the hosts configuration file, which is + named pg_hosts.conf and is stored in the clusters + data directory. The hosts configuration file contains lines of the general + forms: + +hostname SSL_certificate SSL_key SSL_CA_certificate +include file +include_if_exists file +include_dir directory + + Comments, whitespace and line continuations are handled in the same way as in + pg_hba.conf. hostname + is matched againstt the hostname TLS extension in the SSL handshake. + SSL_certificate, + SSL_key, and + SSL_CA_certificate + are treated like + , + , and + respectively. + + diff --git a/src/backend/Makefile b/src/backend/Makefile index 6700aec039..788e9cd65e 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -180,6 +180,7 @@ endif $(MAKE) -C utils install-data $(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample' $(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample' + $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' $(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample' ifeq ($(with_llvm), yes) diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c index 0582606192..957ed03e3b 100644 --- a/src/backend/libpq/be-secure-common.c +++ b/src/backend/libpq/be-secure-common.c @@ -24,8 +24,13 @@ #include "common/percentrepl.h" #include "common/string.h" +#include "libpq/hba.h" #include "libpq/libpq.h" #include "storage/fd.h" +#include "utils/guc.h" +#include "utils/memutils.h" + +static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel); /* * Run ssl_passphrase_command @@ -171,3 +176,154 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart) return true; } + +/* + * parse_hosts_line + * + * Parses a loaded line from the pg_hosts.conf configuration and pulls out the + * hostname, certificate, key and CA parts in order to build an SNI config in + * the TLS backend. Validation of the parsed values is left for the TLS backend + * to implement. + */ +static HostsLine * +parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) +{ + HostsLine *parsedline; + List *tokens; + ListCell *field; + AuthToken *token; + + parsedline = palloc0(sizeof(HostsLine)); + parsedline->sourcefile = pstrdup(tok_line->file_name); + parsedline->linenumber = tok_line->line_num; + parsedline->rawline = pstrdup(tok_line->raw_line); + + /* Hostname */ + field = list_head(tok_line->fields); + tokens = lfirst(field); + token = linitial(tokens); + parsedline->hostname = pstrdup(token->string); + + /* SSL Certificate */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("missing entry at end of line"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + tokens = lfirst(field); + token = linitial(tokens); + parsedline->ssl_cert = pstrdup(token->string); + + /* SSL key */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("missing entry at end of line"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + tokens = lfirst(field); + token = linitial(tokens); + parsedline->ssl_key = pstrdup(token->string); + + /* SSL CA */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("missing entry at end of line"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + tokens = lfirst(field); + token = linitial(tokens); + parsedline->ssl_ca = pstrdup(token->string); + + return parsedline; +} + +/* + * load_hosts + * + * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case + * of errors. + */ +List * +load_hosts(void) +{ + FILE *file; + ListCell *line; + List *hosts_lines = NIL; + List *parsed_lines = NIL; + HostsLine *newline; + bool ok = true; + MemoryContext oldcxt; + MemoryContext hostcxt; + + file = open_auth_file(HostsFileName, LOG, 0, NULL); + if (file == NULL) + { + /* An error has already been logged so no need to add one here. */ + return NIL; + } + + tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0); + + hostcxt = AllocSetContextCreate(PostmasterContext, + "hosts file parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(hostcxt); + + foreach(line, hosts_lines) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line); + + if (tok_line->err_msg != NULL) + { + ok = false; + continue; + } + + if ((newline = parse_hosts_line(tok_line, LOG)) == NULL) + { + ok = false; + continue; + } + + parsed_lines = lappend(parsed_lines, newline); + } + + free_auth_file(file, 0); + MemoryContextSwitchTo(oldcxt); + + /* + * If we didn't find any SNI configuration then that's not an error since + * the pg_hosts file is additive to the default SSL configuration. + */ + if (ok && parsed_lines == NIL) + { + ereport(DEBUG1, + errmsg("no SNI configuration added from configuration file \"%s\"", + HostsFileName)); + MemoryContextDelete(hostcxt); + return NIL; + } + + if (!ok) + { + MemoryContextDelete(hostcxt); + return NIL; + } + + return parsed_lines; +} diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 60cf68aac4..32af7c9f82 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -51,6 +51,13 @@ #endif #include +typedef struct HostContext +{ + const char *hostname; + SSL_CTX *context; + bool default_host; + bool ssl_loaded_verify_locations; +} HostContext; /* default init hook can be overridden by a shared library */ static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart); @@ -73,19 +80,23 @@ static int alpn_cb(SSL *ssl, const unsigned char *in, unsigned int inlen, void *userdata); +static int sni_servername_cb(SSL *ssl, int *al, void *arg); static bool initialize_dh(SSL_CTX *context, bool isServerStart); static bool initialize_ecdh(SSL_CTX *context, bool isServerStart); static const char *SSLerrmessage(unsigned long ecode); static char *X509_NAME_to_cstring(X509_NAME *name); +static List *contexts = NIL; static SSL_CTX *SSL_context = NULL; +static HostContext *Host_context = NULL; static bool SSL_initialized = false; static bool dummy_ssl_passwd_cb_called = false; static bool ssl_is_server_start; static int ssl_protocol_version_to_openssl(int v); static const char *ssl_protocol_version_to_string(int v); +static SSL_CTX *ssl_init_context(bool isServerStart, const char *ctx_ssl_cert_file, const char *ctx_ssl_key_file, const char *ctx_ssl_ca_file); /* for passing data back from verify_cb() */ static const char *cert_errdetail; @@ -97,9 +108,8 @@ static const char *cert_errdetail; int be_tls_init(bool isServerStart) { - SSL_CTX *context; - int ssl_ver_min = -1; - int ssl_ver_max = -1; + SSL_CTX *ctx; + List *sni_hosts = NIL; /* This stuff need be done only once. */ if (!SSL_initialized) @@ -114,6 +124,123 @@ be_tls_init(bool isServerStart) SSL_initialized = true; } + /* + * When ssl_snimode is off or default we load the certificate and key + * specified in postgresql.conf and set that as the default host. + */ + if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT) + { + HostContext *host_context; + + ctx = ssl_init_context(isServerStart, ssl_cert_file, ssl_key_file, ssl_ca_file); + if (ctx == NULL) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load default certificate"))); + return -1; + } + + host_context = palloc0(sizeof(HostContext)); + + host_context->hostname = pstrdup("*"); + host_context->context = ctx; + host_context->default_host = true; + + /* + * Set flag to remember whether CA store has been loaded into + * SSL_context. + */ + if (ssl_ca_file[0]) + host_context->ssl_loaded_verify_locations = true; + + /* + * The contexts list is not used in ssl_snimode off but we add the + * entry there anyways for consistency with the other modes. + */ + contexts = lappend(contexts, host_context); + + /* + * Install the default certificate which for ssl_snimode default can + * be overridden in the callback if a hostname match is found. + */ + SSL_context = ctx; + Host_context = host_context; + } + + /* + * In default or strict ssl_snimode we load all certificates/keys which + * are configured in pg_hosts.conf. In strict mode it is considered a + * fatal error in case there are no configured entries. + */ + if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT) + { + ListCell *line; + + /* + * Load pg_hosts.conf and parse each row, returning the set of hosts + * as a list. + */ + sni_hosts = load_hosts(); + + /* + * In strict ssl_snimode there needs to be a working pg_hosts file, + */ + if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load pg_hosts.conf file"))); + return -1; + } + + foreach(line, sni_hosts) + { + HostContext *host_context; + HostsLine *host = lfirst(line); + SSL_CTX *tmp; + + tmp = ssl_init_context(isServerStart, host->ssl_cert, host->ssl_key, host->ssl_ca); + if (tmp != NULL) + { + SSL_context = tmp; + + host_context = palloc(sizeof(HostContext)); + host_context->hostname = pstrdup(host->hostname); + host_context->context = tmp; + host_context->default_host = false; + + /* + * Set flag to remember whether CA store has been loaded into + * SSL_context. + */ + if (host->ssl_ca) + host_context->ssl_loaded_verify_locations = true; + + contexts = lappend(contexts, host_context); + } + } + } + + /* Make sure we have at least one certificate loaded */ + if (list_length(contexts) < 1) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("no SSL contexts loaded"))); + return -1; + } + + return 0; +} + +static SSL_CTX * +ssl_init_context(bool isServerStart, const char *ctx_ssl_cert_file, const char *ctx_ssl_key_file, const char *ctx_ssl_ca_file) +{ + SSL_CTX *context; + int ssl_ver_min = -1; + int ssl_ver_max = -1; + /* * Create a new SSL context into which we'll load all the configuration * settings. If we fail partway through, we can avoid memory leakage by @@ -139,6 +266,13 @@ be_tls_init(bool isServerStart) */ SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + /* + * Install SNI TLS extension callback in case the server is configured to + * validate hostnames. + */ + if (ssl_snimode != SSL_SNIMODE_OFF) + SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb); + /* * Call init hook (usually to set password callback) */ @@ -150,16 +284,16 @@ be_tls_init(bool isServerStart) /* * Load and verify server's certificate and private key */ - if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1) + if (SSL_CTX_use_certificate_chain_file(context, ctx_ssl_cert_file) != 1) { ereport(isServerStart ? FATAL : LOG, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("could not load server certificate file \"%s\": %s", - ssl_cert_file, SSLerrmessage(ERR_get_error())))); + ctx_ssl_cert_file, SSLerrmessage(ERR_get_error())))); goto error; } - if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart)) + if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart)) goto error; /* @@ -168,19 +302,19 @@ be_tls_init(bool isServerStart) dummy_ssl_passwd_cb_called = false; if (SSL_CTX_use_PrivateKey_file(context, - ssl_key_file, + ctx_ssl_key_file, SSL_FILETYPE_PEM) != 1) { if (dummy_ssl_passwd_cb_called) ereport(isServerStart ? FATAL : LOG, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", - ssl_key_file))); + ctx_ssl_key_file))); else ereport(isServerStart ? FATAL : LOG, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("could not load private key file \"%s\": %s", - ssl_key_file, SSLerrmessage(ERR_get_error())))); + ctx_ssl_key_file, SSLerrmessage(ERR_get_error())))); goto error; } @@ -304,17 +438,17 @@ be_tls_init(bool isServerStart) /* * Load CA store, so we can verify client certificates if needed. */ - if (ssl_ca_file[0]) + if (ctx_ssl_ca_file[0]) { STACK_OF(X509_NAME) * root_cert_list; - if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 || - (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL) + if (SSL_CTX_load_verify_locations(context, ctx_ssl_ca_file, NULL) != 1 || + (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == NULL) { ereport(isServerStart ? FATAL : LOG, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("could not load root certificate file \"%s\": %s", - ssl_ca_file, SSLerrmessage(ERR_get_error())))); + ctx_ssl_ca_file, SSLerrmessage(ERR_get_error())))); goto error; } @@ -386,38 +520,29 @@ be_tls_init(bool isServerStart) } } - /* - * Success! Replace any existing SSL_context. - */ - if (SSL_context) - SSL_CTX_free(SSL_context); - - SSL_context = context; - - /* - * Set flag to remember whether CA store has been loaded into SSL_context. - */ - if (ssl_ca_file[0]) - ssl_loaded_verify_locations = true; - else - ssl_loaded_verify_locations = false; - - return 0; + return context; /* Clean up by releasing working context. */ error: if (context) SSL_CTX_free(context); - return -1; + return NULL; } void be_tls_destroy(void) { - if (SSL_context) - SSL_CTX_free(SSL_context); + ListCell *cell; + + foreach(cell, contexts) + { + HostContext *host_context = lfirst(cell); + + SSL_CTX_free(host_context->context); + pfree(host_context); + } + SSL_context = NULL; - ssl_loaded_verify_locations = false; } int @@ -1346,6 +1471,60 @@ alpn_cb(SSL *ssl, } } +static int +sni_servername_cb(SSL *ssl, int *al, void *arg) +{ + const char *tlsext_hostname; + ListCell *cell; + HostContext *host_context; + + Assert(ssl_snimode != SSL_SNIMODE_OFF); + + tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + + if (!tlsext_hostname) + { + if (ssl_snimode == SSL_SNIMODE_STRICT) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("no hostname provided in callback"))); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + else + return SSL_TLSEXT_ERR_OK; + } + + foreach(cell, contexts) + { + host_context = lfirst(cell); + + if (strcmp(host_context->hostname, tlsext_hostname) == 0) + { + Host_context = host_context; + SSL_context = host_context->context; + SSL_set_SSL_CTX(ssl, SSL_context); + return SSL_TLSEXT_ERR_OK; + } + } + + if (ssl_snimode == SSL_SNIMODE_STRICT) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("no matching pg_hosts entry found for hostname: \"%s\"", + tlsext_hostname))); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + /* + * In ssl_snimode "default" we can return without doing anything since we + * already installed the context for the default host when parsing the + * hosts file. + */ + Assert(SSL_context); + return SSL_TLSEXT_ERR_OK; +} /* * Set DH parameters for generating ephemeral DH keys. The @@ -1539,6 +1718,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len) ptr[0] = '\0'; } +bool +be_tls_loaded_verify_locations(void) +{ + return Host_context->ssl_loaded_verify_locations; +} + char * be_tls_get_certificate_hash(Port *port, size_t *len) { diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index 1663f36b6b..dad1777f92 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -42,10 +42,6 @@ char *ssl_dh_params_file; char *ssl_passphrase_command; bool ssl_passphrase_command_supports_reload; -#ifdef USE_SSL -bool ssl_loaded_verify_locations = false; -#endif - /* GUC variable controlling SSL cipher list */ char *SSLCipherSuites = NULL; @@ -58,6 +54,8 @@ bool SSLPreferServerCiphers; int ssl_min_protocol_version = PG_TLS1_2_VERSION; int ssl_max_protocol_version = PG_TLS_ANY; +int ssl_snimode = SSL_SNIMODE_DEFAULT; + /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ /* ------------------------------------------------------------ */ @@ -97,7 +95,7 @@ bool secure_loaded_verify_locations(void) { #ifdef USE_SSL - return ssl_loaded_verify_locations; + return be_tls_loaded_verify_locations(); #else return false; #endif diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build index 7c65314512..1c6269262c 100644 --- a/src/backend/libpq/meson.build +++ b/src/backend/libpq/meson.build @@ -30,5 +30,6 @@ endif install_data( 'pg_hba.conf.sample', 'pg_ident.conf.sample', + 'pg_hosts.conf.sample', install_dir: dir_data, ) diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample new file mode 100644 index 0000000000..608210686e --- /dev/null +++ b/src/backend/libpq/pg_hosts.conf.sample @@ -0,0 +1,5 @@ +# PostgreSQL SNI Hostname mappings +# ================================ + +# HOSTNAME SSL CERTIFICATE SSL KEY + diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 3fb6803998..4e1a5740d2 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -55,6 +55,7 @@ #define CONFIG_FILENAME "postgresql.conf" #define HBA_FILENAME "pg_hba.conf" #define IDENT_FILENAME "pg_ident.conf" +#define HOSTS_FILENAME "pg_hosts.conf" #ifdef EXEC_BACKEND #define CONFIG_EXEC_PARAMS "global/config_exec_params" @@ -1970,6 +1971,31 @@ SelectConfigFiles(const char *userDoption, const char *progname) } SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); + /* + * Likewise for pg_hosts.conf + */ + if (HostsFileName) + { + fname = make_absolute_path(HostsFileName); + fname_is_malloced = true; + } + else if (configdir) + { + fname = guc_malloc(FATAL, + strlen(configdir) + strlen(HOSTS_FILENAME) + 2); + sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME); + fname_is_malloced = false; + } + else + { + write_stderr("%s does not know where to find the \"hosts\" configuration file.\n" + "This can be specified as \"hosts_file\" in \"%s\", " + "or by the -D invocation option, or by the " + "PGDATA environment variable.\n", + progname, ConfigFileName); + } + SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); + if (fname_is_malloced) free(fname); else diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index ea2b0577bc..92e95ca9b1 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -483,6 +483,13 @@ static const struct config_enum_entry wal_compression_options[] = { {NULL, 0, false} }; +static const struct config_enum_entry ssl_snimode_options[] = { + {"off", SSL_SNIMODE_OFF, false}, + {"default", SSL_SNIMODE_DEFAULT, false}, + {"strict", SSL_SNIMODE_STRICT, false}, + {NULL, 0, false} +}; + /* * Options for enum values stored in other modules */ @@ -540,6 +547,7 @@ char *cluster_name = ""; char *ConfigFileName; char *HbaFileName; char *IdentFileName; +char *HostsFileName; char *external_pid_file; char *application_name; @@ -4476,6 +4484,17 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS, + gettext_noop("Sets the server's \"hosts\" configuration file."), + NULL, + GUC_SUPERUSER_ONLY + }, + &HostsFileName, + NULL, + NULL, NULL, NULL + }, + { {"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS, gettext_noop("Writes the postmaster PID to the specified file."), @@ -5096,6 +5115,18 @@ struct config_enum ConfigureNamesEnum[] = NULL, NULL, NULL }, + { + {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL, + gettext_noop("Sets the SNI mode to use."), + NULL, + GUC_SUPERUSER_ONLY, + }, + &ssl_snimode, + SSL_SNIMODE_DEFAULT, + ssl_snimode_options, + NULL, NULL, NULL + }, + { {"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS, gettext_noop("Sets the method for synchronizing the data directory before crash recovery."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 83d5df8e46..04a4b88a32 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -127,6 +127,7 @@ #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = off +#ssl_snimode = default #------------------------------------------------------------------------------ diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 30e17bd1d1..945d6cbac6 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -176,6 +176,7 @@ static int encodingid; static char *bki_file; static char *hba_file; static char *ident_file; +static char *hosts_file; static char *conf_file; static char *dictionary_file; static char *info_schema_file; @@ -1457,6 +1458,14 @@ setup_config(void) snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data); + writefile(path, conflines); + if (chmod(path, pg_file_create_mode) != 0) + pg_fatal("could not change permissions of \"%s\": %m", path); + + /* pg_hosts.conf */ + conflines = readfile(hosts_file); + snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data); + writefile(path, conflines); if (chmod(path, pg_file_create_mode) != 0) pg_fatal("could not change permissions of \"%s\": %m", path); @@ -2721,6 +2730,7 @@ setup_data_file_paths(void) set_input(&bki_file, "postgres.bki"); set_input(&hba_file, "pg_hba.conf.sample"); set_input(&ident_file, "pg_ident.conf.sample"); + set_input(&hosts_file, "pg_hosts.conf.sample"); set_input(&conf_file, "postgresql.conf.sample"); set_input(&dictionary_file, "snowball_create.sql"); set_input(&info_schema_file, "information_schema.sql"); @@ -2736,12 +2746,13 @@ setup_data_file_paths(void) "PGDATA=%s\nshare_path=%s\nPGPATH=%s\n" "POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n" "POSTGRESQL_CONF_SAMPLE=%s\n" - "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n", + "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n" + "PG_HOSTS_SAMPLE=%s\n", PG_VERSION, pg_data, share_path, bin_path, username, bki_file, conf_file, - hba_file, ident_file); + hba_file, ident_file, hosts_file); if (show_setting) exit(0); } @@ -2749,6 +2760,7 @@ setup_data_file_paths(void) check_input(bki_file); check_input(hba_file); check_input(ident_file); + check_input(hosts_file); check_input(conf_file); check_input(dictionary_file); check_input(info_schema_file); diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 8ea837ae82..2ee6e086bd 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -146,6 +146,20 @@ typedef struct IdentLine AuthToken *pg_user; } IdentLine; +typedef struct HostsLine +{ + int linenumber; + + char *sourcefile; + char *rawline; + + bool default_host; + char *hostname; + char *ssl_key; + char *ssl_cert; + char *ssl_ca; +} HostsLine; + /* * TokenizedAuthLine represents one line lexed from an authentication * configuration file. Each item in the "fields" list is a sub-list of diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 05cb1874c5..75ba89fcef 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -308,6 +308,7 @@ extern const char *be_tls_get_cipher(Port *port); extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len); extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len); extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len); +extern bool be_tls_loaded_verify_locations(void); /* * Get the server certificate hash for SCRAM channel binding type diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index 83e338f604..a896b37adf 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -122,6 +122,7 @@ extern PGDLLIMPORT char *SSLECDHCurve; extern PGDLLIMPORT bool SSLPreferServerCiphers; extern PGDLLIMPORT int ssl_min_protocol_version; extern PGDLLIMPORT int ssl_max_protocol_version; +extern PGDLLIMPORT int ssl_snimode; enum ssl_protocol_versions { @@ -132,6 +133,13 @@ enum ssl_protocol_versions PG_TLS1_3_VERSION, }; +enum ssl_snimode +{ + SSL_SNIMODE_OFF = 0, + SSL_SNIMODE_DEFAULT, + SSL_SNIMODE_STRICT +}; + /* * prototypes for functions in be-secure-common.c */ @@ -139,5 +147,6 @@ extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size); extern bool check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart); +extern List *load_hosts(void); #endif /* LIBPQ_H */ diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index e4a594b5e8..1e420c3df3 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -276,6 +276,7 @@ extern PGDLLIMPORT char *cluster_name; extern PGDLLIMPORT char *ConfigFileName; extern PGDLLIMPORT char *HbaFileName; extern PGDLLIMPORT char *IdentFileName; +extern PGDLLIMPORT char *HostsFileName; extern PGDLLIMPORT char *external_pid_file; extern PGDLLIMPORT char *application_name; diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build index b3c5503f79..55f5887d9c 100644 --- a/src/test/ssl/meson.build +++ b/src/test/ssl/meson.build @@ -13,6 +13,7 @@ tests += { 't/001_ssltests.pl', 't/002_scram.pl', 't/003_sslinfo.pl', + 't/004_sni.pl', ], }, } diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl new file mode 100644 index 0000000000..d95af50376 --- /dev/null +++ b/src/test/ssl/t/004_sni.pl @@ -0,0 +1,86 @@ + +# Copyright (c) 2024, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use SSL::Server; + +# This is the hostname used to connect to the server. This cannot be a +# hostname, because the server certificate is always for the domain +# postgresql-ssl-regression.test. +my $SERVERHOSTADDR = '127.0.0.1'; +# This is the pattern to use in pg_hba.conf to match incoming connections. +my $SERVERHOSTCIDR = '127.0.0.1/32'; + +if ($ENV{with_ssl} ne 'openssl') +{ + plan skip_all => 'OpenSSL not supported by this build'; +} + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/) +{ + plan skip_all => + 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA'; +} + +sub reset_pg_hosts +{ + my $node = shift; + + ok(unlink($node->data_dir . '/pg_hosts.conf')); + $node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt"); + $node->reload; + return; +} + +my $ssl_server = SSL::Server->new(); + +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init; + +# PGHOST is enforced here to set up the node, subsequent connections +# will use a dedicated connection string. +$ENV{PGHOST} = $node->host; +$ENV{PGPORT} = $node->port; +$node->start; + +$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, + $SERVERHOSTCIDR, 'trust'); + +$ssl_server->switch_server_cert($node, certfile => 'server-cn-only'); + +my $connstr = + "dbname=trustdb hostaddr=$SERVERHOSTADDR host=localhost sslsni=1"; + +$node->append_conf('postgresql.conf', "ssl_snimode=default"); +$node->reload; + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); + +$node->append_conf('postgresql.conf', "ssl_snimode=strict"); +$node->reload; + +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require", + expected_stderr => qr/unrecognized name/); + +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', "localhost server-cn-only.crt server-cn-only.key root_ca.crt"); +$node->reload; + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); + +done_testing(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 2311f82d81..c4e9e88259 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1140,6 +1140,8 @@ HeapTupleHeader HeapTupleHeaderData HeapTupleTableSlot HistControl +HostContext +HostsLine HotStandbyState I32 ICU_Convert_Func -- 2.39.3 (Apple Git-146)