Skip to content

Commit 015162e

Browse files
author
Commitfest Bot
committed
[CF 4984] v6 - Serverside SNI for SSL connections
This branch was automatically generated by a robot using patches from an email thread registered at: https://commitfest.postgresql.org/patch/4984 The branch will be overwritten each time a new patch version is posted to the thread, and also periodically to check for bitrot caused by changes on the master branch. Patch(es): https://www.postgresql.org/message-id/[email protected] Author(s): Daniel Gustafsson
2 parents 2c0ed86 + 69b7f47 commit 015162e

File tree

20 files changed

+937
-50
lines changed

20 files changed

+937
-50
lines changed

doc/src/sgml/config.sgml

+66
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,72 @@ include_dir 'conf.d'
16781678
</para>
16791679
</listitem>
16801680
</varlistentry>
1681+
1682+
<varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
1683+
<term><varname>ssl_snimode</varname> (<type>enum</type>)
1684+
<indexterm>
1685+
<primary><varname>ssl_snimode</varname> configuration parameter</primary>
1686+
</indexterm>
1687+
</term>
1688+
<listitem>
1689+
<para>
1690+
This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
1691+
when establishing the connection, and how it should be interpreted.
1692+
Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
1693+
</para>
1694+
<para>
1695+
<variablelist>
1696+
<varlistentry id="guc-ssl-snimode-off">
1697+
<term><literal>off</literal></term>
1698+
<listitem>
1699+
<para>
1700+
SNI is not enabled and no configuration from
1701+
<filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
1702+
for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
1703+
<xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
1704+
</para>
1705+
</listitem>
1706+
</varlistentry>
1707+
1708+
<varlistentry id="guc-ssl-snimode-default">
1709+
<term><literal>default</literal></term>
1710+
<listitem>
1711+
<para>
1712+
SNI is enabled and hostname configuration is loaded from
1713+
<filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
1714+
<xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
1715+
are loaded as the default configuration. Connections specifying
1716+
<xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
1717+
will be attempted using the default configuration if the hostname
1718+
is missing in <filename>pg_hosts.conf</filename>. If the hostname
1719+
matches an entry from <filename>pg_hosts.conf</filename>, then the
1720+
configuration from that entry will be used for setting up the
1721+
connection.
1722+
</para>
1723+
</listitem>
1724+
</varlistentry>
1725+
1726+
<varlistentry id="guc-ssl-snimode-strict">
1727+
<term><literal>strict</literal></term>
1728+
<listitem>
1729+
<para>
1730+
SNI is enabled and all connections are required to set <xref
1731+
linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
1732+
specify a hostname matching an entry in
1733+
<filename>pg_hosts.conf</filename>. Any connection without <xref
1734+
linkend="libpq-connect-sslsni"/> or with a hostname missing from
1735+
<filename>pg_hosts.conf</filename> will be rejected.
1736+
<xref linkend="guc-ssl-cert-file"/>,
1737+
<xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
1738+
are loaded in order to drive the handshake until the appropriate
1739+
configuration has been selected.
1740+
</para>
1741+
</listitem>
1742+
</varlistentry>
1743+
</variablelist>
1744+
</para>
1745+
</listitem>
1746+
</varlistentry>
16811747
</variablelist>
16821748
</sect2>
16831749
</sect1>

doc/src/sgml/runtime.sgml

+67
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
24452445
<entry>client certificate must not be on this list</entry>
24462446
</row>
24472447

2448+
<row>
2449+
<entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
2450+
<entry>SNI configuration</entry>
2451+
<entry>defines which certificates to use for which server hostname</entry>
2452+
</row>
2453+
24482454
</tbody>
24492455
</tgroup>
24502456
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
25722578
</para>
25732579
</sect2>
25742580

2581+
<sect2 id="ssl-sni">
2582+
<title>SNI Configuration</title>
2583+
2584+
<para>
2585+
<productname>PostgreSQL</productname> can be configured for
2586+
<acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
2587+
configuration file. <productname>PostgreSQL</productname> inspects the TLS
2588+
hostname extension in the SSL connection handshake, and selects the right
2589+
TLS certificate, key and CA certificate to use for the connection.
2590+
</para>
2591+
2592+
<para>
2593+
SNI configuration is defined in the hosts configuration file,
2594+
<filename>pg_hosts.conf</filename>, which is stored in the clusters
2595+
data directory. The hosts configuration file contains lines of the general
2596+
forms:
2597+
<synopsis>
2598+
<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>
2599+
<replaceable>include</replaceable> <replaceable>file</replaceable>
2600+
<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
2601+
<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
2602+
</synopsis>
2603+
Comments, whitespace and line continuations are handled in the same way as
2604+
in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
2605+
is matched against the hostname TLS extension in the SSL handshake.
2606+
<replaceable>SSL_certificate</replaceable>,
2607+
<replaceable>SSL_key</replaceable>,
2608+
<replaceable>SSL_CA_certificate</replaceable>,
2609+
<replaceable>SSL_passphrase_cmd</replaceable>, and
2610+
<replaceable>SSL_passphrase_cmd_reload</replaceable>
2611+
are treated like
2612+
<xref linkend="guc-ssl-cert-file"/>,
2613+
<xref linkend="guc-ssl-key-file"/>,
2614+
<xref linkend="guc-ssl-ca-file"/>,
2615+
<xref linkend="guc-ssl-passphrase-command"/>, and
2616+
<xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
2617+
All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
2618+
<replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
2619+
<replaceable>SSL_passphrase_cmd</replaceable> is defined but not
2620+
<replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
2621+
value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
2622+
<literal>off</literal>.
2623+
</para>
2624+
<para>
2625+
The SSL configuration from <filename>postgresql.conf</filename> is used
2626+
in order to set up the TLS handshake such that the hostname extension can
2627+
be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
2628+
<literal>default</literal> this configuration will be the defualt fallback
2629+
if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
2630+
<xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
2631+
will only be used to for the handshake until the hostname is inspected, it
2632+
will not be used for the connection.
2633+
</para>
2634+
<para>
2635+
It is currently not possible to set different <literal>clientname</literal>
2636+
values for the different certificates. Any <literal>clientname</literal>
2637+
setting in <filename>pg_hba.conf</filename> will be applied during
2638+
authentication regardless of which set of certificates have been loaded
2639+
via an SNI enabled connection.
2640+
</para>
2641+
</sect2>
25752642
</sect1>
25762643

25772644
<sect1 id="gssapi-enc">

src/backend/Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ endif
187187
$(MAKE) -C utils install-data
188188
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
189189
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
190+
$(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
190191
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
191192

192193
ifeq ($(with_llvm), yes)

src/backend/libpq/be-secure-common.c

+201-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@
2424

2525
#include "common/percentrepl.h"
2626
#include "common/string.h"
27+
#include "libpq/hba.h"
2728
#include "libpq/libpq.h"
2829
#include "storage/fd.h"
30+
#include "utils/guc.h"
31+
#include "utils/memutils.h"
32+
33+
static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
2934

3035
/*
3136
* Run ssl_passphrase_command
@@ -37,19 +42,20 @@
3742
* value is the length of the actual result.
3843
*/
3944
int
40-
run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
45+
run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size, void *userdata)
4146
{
4247
int loglevel = is_server_start ? ERROR : LOG;
4348
char *command;
4449
FILE *fh;
4550
int pclose_rc;
4651
size_t len = 0;
52+
char *cmd = (char *) userdata;
4753

4854
Assert(prompt);
4955
Assert(size > 0);
5056
buf[0] = '\0';
5157

52-
command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
58+
command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
5359

5460
fh = OpenPipeStream(command, "r");
5561
if (fh == NULL)
@@ -175,3 +181,196 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
175181

176182
return true;
177183
}
184+
185+
/*
186+
* parse_hosts_line
187+
*
188+
* Parses a loaded line from the pg_hosts.conf configuration and pulls out the
189+
* hostname, certificate, key and CA parts in order to build an SNI config in
190+
* the TLS backend. Validation of the parsed values is left for the TLS backend
191+
* to implement.
192+
*/
193+
static HostsLine *
194+
parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
195+
{
196+
HostsLine *parsedline;
197+
List *tokens;
198+
ListCell *field;
199+
AuthToken *token;
200+
201+
parsedline = palloc0(sizeof(HostsLine));
202+
parsedline->sourcefile = pstrdup(tok_line->file_name);
203+
parsedline->linenumber = tok_line->line_num;
204+
parsedline->rawline = pstrdup(tok_line->raw_line);
205+
206+
/* Initialize optional fields */
207+
parsedline->ssl_passphrase_cmd = NULL;
208+
parsedline->ssl_passphrase_reload = false;
209+
210+
/* Hostname */
211+
field = list_head(tok_line->fields);
212+
tokens = lfirst(field);
213+
token = linitial(tokens);
214+
parsedline->hostname = pstrdup(token->string);
215+
216+
/* SSL Certificate (Required) */
217+
field = lnext(tok_line->fields, field);
218+
if (!field)
219+
{
220+
ereport(elevel,
221+
errcode(ERRCODE_CONFIG_FILE_ERROR),
222+
errmsg("missing entry at end of line"),
223+
errcontext("line %d of configuration file \"%s\"",
224+
tok_line->line_num, tok_line->file_name));
225+
return NULL;
226+
}
227+
tokens = lfirst(field);
228+
token = linitial(tokens);
229+
parsedline->ssl_cert = pstrdup(token->string);
230+
231+
/* SSL key (Required) */
232+
field = lnext(tok_line->fields, field);
233+
if (!field)
234+
{
235+
ereport(elevel,
236+
errcode(ERRCODE_CONFIG_FILE_ERROR),
237+
errmsg("missing entry at end of line"),
238+
errcontext("line %d of configuration file \"%s\"",
239+
tok_line->line_num, tok_line->file_name));
240+
return NULL;
241+
}
242+
tokens = lfirst(field);
243+
token = linitial(tokens);
244+
parsedline->ssl_key = pstrdup(token->string);
245+
246+
/* SSL CA (Required) */
247+
field = lnext(tok_line->fields, field);
248+
if (!field)
249+
{
250+
ereport(elevel,
251+
errcode(ERRCODE_CONFIG_FILE_ERROR),
252+
errmsg("missing entry at end of line"),
253+
errcontext("line %d of configuration file \"%s\"",
254+
tok_line->line_num, tok_line->file_name));
255+
return NULL;
256+
}
257+
tokens = lfirst(field);
258+
token = linitial(tokens);
259+
parsedline->ssl_ca = pstrdup(token->string);
260+
261+
/* SSL Passphrase Command (optional) */
262+
field = lnext(tok_line->fields, field);
263+
if (field)
264+
{
265+
tokens = lfirst(field);
266+
token = linitial(tokens);
267+
parsedline->ssl_passphrase_cmd = pstrdup(token->string);
268+
269+
/*
270+
* SSL Passphrase Command support reload (optional). This field is
271+
* only supported if there was a passphrase command parsed first, so
272+
* nest it under the previous token.
273+
*/
274+
field = lnext(tok_line->fields, field);
275+
if (field)
276+
{
277+
tokens = lfirst(field);
278+
token = linitial(tokens);
279+
280+
if (token->string[0] == '1'
281+
|| pg_strcasecmp(token->string, "true") == 0
282+
|| pg_strcasecmp(token->string, "on") == 0
283+
|| pg_strcasecmp(token->string, "yes") == 0)
284+
parsedline->ssl_passphrase_reload = true;
285+
else if (token->string[0] == '0'
286+
|| pg_strcasecmp(token->string, "false") == 0
287+
|| pg_strcasecmp(token->string, "off") == 0
288+
|| pg_strcasecmp(token->string, "no") == 0)
289+
parsedline->ssl_passphrase_reload = false;
290+
else
291+
ereport(elevel,
292+
errcode(ERRCODE_CONFIG_FILE_ERROR),
293+
errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
294+
errcontext("line %d of configuration file \"%s\"",
295+
tok_line->line_num, tok_line->file_name));
296+
}
297+
}
298+
299+
return parsedline;
300+
}
301+
302+
/*
303+
* load_hosts
304+
*
305+
* Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case
306+
* of errors.
307+
*/
308+
List *
309+
load_hosts(void)
310+
{
311+
FILE *file;
312+
ListCell *line;
313+
List *hosts_lines = NIL;
314+
List *parsed_lines = NIL;
315+
HostsLine *newline;
316+
bool ok = true;
317+
MemoryContext oldcxt;
318+
MemoryContext hostcxt;
319+
320+
file = open_auth_file(HostsFileName, LOG, 0, NULL);
321+
if (file == NULL)
322+
{
323+
/* An error has already been logged so no need to add one here */
324+
return NIL;
325+
}
326+
327+
tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
328+
329+
hostcxt = AllocSetContextCreate(PostmasterContext,
330+
"hosts file parser context",
331+
ALLOCSET_SMALL_SIZES);
332+
oldcxt = MemoryContextSwitchTo(hostcxt);
333+
334+
foreach(line, hosts_lines)
335+
{
336+
TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
337+
338+
if (tok_line->err_msg != NULL)
339+
{
340+
ok = false;
341+
continue;
342+
}
343+
344+
if ((newline = parse_hosts_line(tok_line, LOG)) == NULL)
345+
{
346+
ok = false;
347+
continue;
348+
}
349+
350+
parsed_lines = lappend(parsed_lines, newline);
351+
}
352+
353+
free_auth_file(file, 0);
354+
MemoryContextSwitchTo(oldcxt);
355+
356+
/*
357+
* If we didn't find any SNI configuration then that's not an error since
358+
* the pg_hosts file is additive to the default SSL configuration.
359+
*/
360+
if (ok && parsed_lines == NIL)
361+
{
362+
ereport(DEBUG1,
363+
errmsg("no SNI configuration added from configuration file \"%s\"",
364+
HostsFileName));
365+
MemoryContextDelete(hostcxt);
366+
return NIL;
367+
}
368+
369+
if (!ok)
370+
{
371+
MemoryContextDelete(hostcxt);
372+
return NIL;
373+
}
374+
375+
return parsed_lines;
376+
}

0 commit comments

Comments
 (0)