Add ssl_passphrase_command setting

This allows specifying an external command for prompting for or
otherwise obtaining passphrases for SSL key files.  This is useful
because in many cases there is no TTY easily available during service
startup.

Also add a setting ssl_passphrase_command_supports_reload, which allows
supporting SSL configuration reload even if SSL files need passphrases.

Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
This commit is contained in:
Peter Eisentraut 2018-02-26 13:28:38 -05:00
parent 7a50bb690b
commit 8a3d942529
13 changed files with 313 additions and 20 deletions

View File

@ -1313,6 +1313,66 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
<varlistentry id="guc-ssl-passphrase-command" xreflabel="ssl_passphrase_command">
<term><varname>ssl_passphrase_command</varname> (<type>string</type>)
<indexterm>
<primary><varname>ssl_passphrase_command</varname> configuration parameter</primary>
</indexterm>
</term>
<listitem>
<para>
Sets an external command to be invoked when a passphrase for
decrypting an SSL file such as a private key needs to be obtained. By
default, this parameter is empty, which means the built-in prompting
mechanism is used.
</para>
<para>
The command must print the passphrase to the standard output and exit
with code 0. In the parameter value, <literal>%p</literal> is
replaced by a prompt string. (Write <literal>%%</literal> for a
literal <literal>%</literal>.) Note that the prompt string will
probably contain whitespace, so be sure to quote adequately. A single
newline is stripped from the end of the output if present.
</para>
<para>
The command does not actually have to prompt the user for a
passphrase. It can read it from a file, obtain it from a keychain
facility, or similar. It is up to the user to make sure the chosen
mechanism is adequately secure.
</para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
</para>
</listitem>
</varlistentry>
<varlistentry id="guc-ssl-passphrase-command-supports-reload" xreflabel="ssl_passphrase_command_supports_reload">
<term><varname>ssl_passphrase_command_supports_reload</varname> (<type>boolean</type>)
<indexterm>
<primary><varname>ssl_passphrase_command_supports_reload</varname> configuration parameter</primary>
</indexterm>
</term>
<listitem>
<para>
This setting determines whether the passphrase command set by
<varname>ssl_passphrase_command</varname> will also be called during a
configuration reload if a key file needs a passphrase. If this
setting is false (the default), then
<varname>ssl_passphrase_command</varname> will be ignored during a
reload and the SSL configuration will not be reloaded if a passphrase
is needed. That setting is appropriate for a command that requires a
TTY for prompting, which might not be available when the server is
running. Setting this to true might be appropriate if the passphrase
is obtained from a file, for example.
</para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
</sect1>

View File

@ -14,7 +14,7 @@ include $(top_builddir)/src/Makefile.global
# be-fsstubs is here for historical reasons, probably belongs elsewhere
OBJS = be-fsstubs.o be-secure.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \
OBJS = be-fsstubs.o be-secure.o be-secure-common.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \
pqformat.o pqmq.o pqsignal.o auth-scram.o
ifeq ($(with_openssl),yes)

View File

@ -0,0 +1,120 @@
/*-------------------------------------------------------------------------
*
* be-secure-common.c
*
* common implementation-independent SSL support code
*
* While be-secure.c contains the interfaces that the rest of the
* communications code calls, this file contains support routines that are
* used by the library-specific implementations such as be-secure-openssl.c.
*
* Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* IDENTIFICATION
* src/backend/libpq/be-secure-common.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
* of error messages.
*
* 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)
{
int loglevel = is_server_start ? ERROR : LOG;
StringInfoData command;
char *p;
FILE *fh;
int pclose_rc;
size_t len = 0;
Assert(prompt);
Assert(size > 0);
buf[0] = '\0';
initStringInfo(&command);
for (p = ssl_passphrase_command; *p; p++)
{
if (p[0] == '%')
{
switch (p[1])
{
case 'p':
appendStringInfoString(&command, prompt);
p++;
break;
case '%':
appendStringInfoChar(&command, '%');
p++;
break;
default:
appendStringInfoChar(&command, p[0]);
}
}
else
appendStringInfoChar(&command, p[0]);
}
fh = OpenPipeStream(command.data, "r");
if (fh == NULL)
{
ereport(loglevel,
(errcode_for_file_access(),
errmsg("could not execute command \"%s\": %m",
command.data)));
goto error;
}
if (!fgets(buf, size, fh))
{
if (ferror(fh))
{
ereport(loglevel,
(errcode_for_file_access(),
errmsg("could not read from command \"%s\": %m",
command.data)));
goto error;
}
}
pclose_rc = ClosePipeStream(fh);
if (pclose_rc == -1)
{
ereport(loglevel,
(errcode_for_file_access(),
errmsg("could not close pipe to external command: %m")));
goto error;
}
else if (pclose_rc != 0)
{
ereport(loglevel,
(errcode_for_file_access(),
errmsg("command \"%s\" failed",
command.data),
errdetail_internal("%s", wait_result_to_str(pclose_rc))));
goto error;
}
/* strip trailing newline */
len = strlen(buf);
if (buf[len - 1] == '\n')
buf[len-- -1] = '\0';
error:
pfree(command.data);
return len;
}

View File

@ -52,7 +52,8 @@ static int my_SSL_set_fd(Port *port, int fd);
static DH *load_dh_file(char *filename, bool isServerStart);
static DH *load_dh_buffer(const char *, size_t);
static int ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata);
static int ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata);
static int dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata);
static int verify_cb(int, X509_STORE_CTX *);
static void info_cb(const SSL *ssl, int type, int args);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
@ -63,7 +64,8 @@ static char *X509_NAME_to_cstring(X509_NAME *name);
static SSL_CTX *SSL_context = NULL;
static bool SSL_initialized = false;
static bool ssl_passwd_cb_called = false;
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
/* ------------------------------------------------------------ */
@ -111,14 +113,28 @@ be_tls_init(bool isServerStart)
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
/*
* If reloading, override OpenSSL's default handling of
* passphrase-protected files, because we don't want to prompt for a
* passphrase in an already-running server. (Not that the default
* handling is very desirable during server start either, but some people
* insist we need to keep it.)
* Set password callback
*/
if (!isServerStart)
SSL_CTX_set_default_passwd_cb(context, ssl_passwd_cb);
if (isServerStart)
{
if (ssl_passphrase_command[0])
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
}
else
{
if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
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(context, dummy_ssl_passwd_cb);
}
/* used by the callback */
ssl_is_server_start = isServerStart;
/*
* Load and verify server's certificate and private key
@ -138,13 +154,13 @@ be_tls_init(bool isServerStart)
/*
* OK, try to load the private key file.
*/
ssl_passwd_cb_called = false;
dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
if (ssl_passwd_cb_called)
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",
@ -839,7 +855,21 @@ load_dh_buffer(const char *buffer, size_t len)
}
/*
* Passphrase collection callback
* Passphrase collection callback using ssl_passphrase_command
*/
static int
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:";
Assert(rwflag == 0);
return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
}
/*
* Dummy passphrase callback
*
* If OpenSSL is told to use a passphrase-protected server key, by default
* it will issue a prompt on /dev/tty and try to read a key from there.
@ -848,10 +878,10 @@ load_dh_buffer(const char *buffer, size_t len)
* function that just returns an empty passphrase, guaranteeing failure.
*/
static int
ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
{
/* Set flag to change the error message we'll report */
ssl_passwd_cb_called = true;
dummy_ssl_passwd_cb_called = true;
/* And return empty string */
Assert(size > 0);
buf[0] = '\0';

View File

@ -45,6 +45,8 @@ char *ssl_key_file;
char *ssl_ca_file;
char *ssl_crl_file;
char *ssl_dh_params_file;
char *ssl_passphrase_command;
bool ssl_passphrase_command_supports_reload;
#ifdef USE_SSL
bool ssl_loaded_verify_locations = false;

View File

@ -988,6 +988,15 @@ static struct config_bool ConfigureNamesBool[] =
false,
check_ssl, NULL, NULL
},
{
{"ssl_passphrase_command_supports_reload", PGC_SIGHUP, CONN_AUTH_SSL,
gettext_noop("Also use ssl_passphrase_command during server reload."),
NULL
},
&ssl_passphrase_command_supports_reload,
false,
NULL, NULL, NULL
},
{
{"ssl_prefer_server_ciphers", PGC_SIGHUP, CONN_AUTH_SSL,
gettext_noop("Give priority to server ciphersuite order."),
@ -3655,6 +3664,16 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
{
{"ssl_passphrase_command", PGC_SIGHUP, CONN_AUTH_SSL,
gettext_noop("Command to obtain passphrases for SSL."),
NULL
},
&ssl_passphrase_command,
"",
NULL, NULL, NULL
},
{
{"application_name", PGC_USERSET, LOGGING_WHAT,
gettext_noop("Sets the application name to be reported in statistics and logs."),

View File

@ -104,6 +104,8 @@
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------

View File

@ -80,6 +80,8 @@ extern char *ssl_key_file;
extern char *ssl_ca_file;
extern char *ssl_crl_file;
extern char *ssl_dh_params_file;
extern char *ssl_passphrase_command;
extern bool ssl_passphrase_command_supports_reload;
extern int secure_initialize(bool isServerStart);
extern bool secure_loaded_verify_locations(void);
@ -101,4 +103,10 @@ extern char *SSLCipherSuites;
extern char *SSLECDHCurve;
extern bool SSLPreferServerCiphers;
/*
* 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);
#endif /* LIBPQ_H */

View File

@ -22,6 +22,7 @@ CERTIFICATES := server_ca server-cn-and-alt-names \
root_ca
SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \
ssl/server-password.key \
ssl/client.crl ssl/server.crl ssl/root.crl \
ssl/both-cas-1.crt ssl/both-cas-2.crt \
ssl/root+server_ca.crt ssl/root+server.crl \
@ -71,6 +72,10 @@ ssl/server-ss.crt: ssl/server-cn-only.key ssl/server-cn-only.crt server-cn-only.
openssl x509 -req -days 10000 -in ssl/server-ss.csr -signkey ssl/server-cn-only.key -out ssl/server-ss.crt -extensions v3_req -extfile server-cn-only.config
rm ssl/server-ss.csr
# Password-protected version of server-cn-only.key
ssl/server-password.key: ssl/server-cn-only.key
openssl rsa -des -in $< -out $@ -passout 'pass:secret1'
# Client certificate, signed by the client CA:
ssl/client.crt: ssl/client.key ssl/client_ca.crt
openssl req -new -key ssl/client.key -out ssl/client.csr -config client.config

View File

@ -48,6 +48,9 @@ server-no-names
server-ss
same as server-cn-only, but self-signed.
server-password
same as server-cn-only, but password-protected.
client
a client certificate, for user "ssltestuser". Signed by client_ca.

View File

@ -0,0 +1,18 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-CBC,2FAEFD1C1B2C881C
PGi9r3pm05iUwz5QbZik+ZNu0fHNaX8LJFZqpOhg0TV38csLtQ2PRjZ0Q/diBlVT
SD8JJnIvwPoIWXyMMTax/krFL0CpbFqgAzD4CEgfWxGNhwnMD1DkNaYp/UF/NfuF
7TqXomUlcH/pVaZlu7G0wrIo5rnjef70I7GEY2vwT5adSLsUBAgrs/u3MAAx/Wh4
PkVxZELmyiH/8MdIevodjRcJrgIzRheEph39eHrWKgWeSbO0DEQK91vv3prICwo2
w2iU0Zohf92QuquA2MKZWruCHb4A4HusUZf3Zc14Yueu/HyztSrHmFeBp0amlWep
/o6mx274XVj7IpanOPPM4qEhrF97LHdaSEPn9HwxvvV4GFJDNCVEBl4zuaHo0N8C
85GPazIxUWB3CB9PrtXduxeI22lwrIiUdmzA68EXHD7Wg8R90397MNMOomLgfNcu
rXarrTXmTNgOa20hc1Ue5AXg9fVS9V/5GP4Dn9SX/CdaE1rz0b73N/ViQzVrS9Ne
n04qYPbnf+MQmFWnzMXctZbYG6jDCbuGFIGP4i/LG+wOE8Rntu8Re9re+HANu5VJ
Ht20wYOGZIpNwo4YenxvPeTTlbB0Qcma2lnw2bt19owpNQVIeTnRQXxZs3/Y3a+A
+/B8VvIkQ0u0EpnSVLBetEmJqtOQvBz7c4Z+0Cl+DL1bTqrDn54MxUBap6dgU+/1
R6pxx1F0ZTtQauVmO8n3rWKwOGG5NeMhf4iId2JWpw39VtRk8LNtnGUbUAbL5znY
rkUVyJstQg6U6kNTgDWQ1nBxCzlRz2xpHyghnyxLkMpW5ECpmwwLDQ==
-----END RSA PRIVATE KEY-----

View File

@ -8,7 +8,7 @@ use File::Copy;
if ($ENV{with_openssl} eq 'yes')
{
plan tests => 62;
plan tests => 64;
}
else
{
@ -38,7 +38,7 @@ chmod 0600, "ssl/client-revoked_tmp.key";
copy("ssl/client.key", "ssl/client_wrongperms_tmp.key");
chmod 0644, "ssl/client_wrongperms_tmp.key";
#### Part 0. Set up the server.
#### Set up the server.
note "setting up data directory";
my $node = get_new_node('master');
@ -50,9 +50,32 @@ $ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
configure_test_server_for_ssl($node, $SERVERHOSTADDR, 'trust');
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
note "testing password-protected keys";
open my $sslconf, '>', $node->data_dir."/sslconfig.conf";
print $sslconf "ssl=on\n";
print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
print $sslconf "ssl_key_file='server-password.key'\n";
print $sslconf "ssl_passphrase_command='echo wrongpassword'\n";
close $sslconf;
command_fails(['pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart'],
'restart fails with password-protected key file with wrong password');
$node->_update_pid(0);
open $sslconf, '>', $node->data_dir."/sslconfig.conf";
print $sslconf "ssl=on\n";
print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
print $sslconf "ssl_key_file='server-password.key'\n";
print $sslconf "ssl_passphrase_command='echo secret1'\n";
close $sslconf;
command_ok(['pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart'],
'restart succeeds with password-protected key file');
$node->_update_pid(1);
### Run client-side tests.
###
### Test that libpq accepts/rejects the connection correctly, depending
### on sslmode and whether the server's certificate looks correct. No
@ -60,6 +83,8 @@ switch_server_cert($node, 'server-cn-only');
note "running client tests";
switch_server_cert($node, 'server-cn-only');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@ -235,7 +260,7 @@ test_connect_fails($common_connstr,
qr/SSL error/,
"does not connect with client-side CRL");
### Part 2. Server-side tests.
### Server-side tests.
###
### Test certificate authorization.

View File

@ -182,6 +182,7 @@ sub mkvcbuild
# if building without OpenSSL
if (!$solution->{options}->{openssl})
{
$postgres->RemoveFile('src/backend/libpq/be-secure-common.c');
$postgres->RemoveFile('src/backend/libpq/be-secure-openssl.c');
}