WIP-log-authenticated-identity-from-multiple-auth-ba.patch
text/x-patch
Filename: WIP-log-authenticated-identity-from-multiple-auth-ba.patch
Type: text/x-patch
Part: 0
Patch
Format: format-patch
Subject: WIP: log authenticated identity from multiple auth backends
| File | + | − |
|---|---|---|
| src/backend/libpq/auth.c | 19 | 2 |
| src/backend/libpq/be-secure-openssl.c | 22 | 0 |
| src/backend/utils/error/elog.c | 18 | 0 |
| src/include/libpq/libpq-be.h | 12 | 0 |
| src/test/kerberos/t/001_auth.pl | 14 | 1 |
| src/test/ldap/t/001_auth.pl | 21 | 1 |
| src/test/ssl/t/001_ssltests.pl | 42 | 1 |
| src/test/ssl/t/002_scram.pl | 52 | 2 |
From 3f6e87a744be339748fc707cd071896e81e0323c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Fri, 22 Jan 2021 14:03:14 -0800
Subject: [PATCH] WIP: log authenticated identity from multiple auth backends
This is stored into port->authn_id according to the auth method:
ldap: the final bind DN
gss: the user principal
cert: the client's Subject DN
scram-sha-256: the Postgres username
It can be logged with the %Z specifier in log_line_prefix.
---
src/backend/libpq/auth.c | 21 ++++++++++-
src/backend/libpq/be-secure-openssl.c | 22 +++++++++++
src/backend/utils/error/elog.c | 18 +++++++++
src/include/libpq/libpq-be.h | 12 ++++++
src/test/kerberos/t/001_auth.pl | 15 +++++++-
src/test/ldap/t/001_auth.pl | 22 ++++++++++-
src/test/ssl/t/001_ssltests.pl | 43 ++++++++++++++++++++-
src/test/ssl/t/002_scram.pl | 54 ++++++++++++++++++++++++++-
8 files changed, 200 insertions(+), 7 deletions(-)
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 545635f41a..2bff140d3c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1021,6 +1021,10 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
return STATUS_ERROR;
}
+ /* TODO: move this up the stack so that it's shared by other password
+ * authentication methods, once tests are written. */
+ port->authn_id = port->user_name;
+
return STATUS_OK;
}
@@ -1208,9 +1212,10 @@ pg_GSS_checkauth(Port *port)
/*
* Copy the original name of the authenticated principal into our backend
- * memory for display later.
+ * memory for display later. This is also our authenticated identity.
*/
- port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+ port->authn_id = port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+
/*
* Split the username at the realm separator
@@ -2809,6 +2814,9 @@ CheckLDAPAuth(Port *port)
return STATUS_ERROR;
}
+ /* Save the original bind DN as the authenticated identity. */
+ port->authn_id = MemoryContextStrdup(TopMemoryContext, fulluser);
+
ldap_unbind(ldap);
pfree(passwd);
pfree(fulluser);
@@ -2861,6 +2869,15 @@ CheckCertAuth(Port *port)
return STATUS_ERROR;
}
+ if (port->hba->auth_method == uaCert)
+ {
+ /*
+ * The client's Subject DN is our authenticated identity. Unfortunately
+ * there is no canonical string representation; do the best we can.
+ */
+ port->authn_id = be_tls_get_peer_full_subject_name(port);
+ }
+
/* Just pass the certificate cn to the usermap check */
status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
if (status_check_usermap != STATUS_OK)
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e2ecc6e7a..1c7f74dc22 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -562,12 +562,34 @@ aloop:
port->peer_cn = peer_cn;
}
+
port->peer_cert_valid = true;
}
return 0;
}
+const char *
+be_tls_get_peer_full_subject_name(Port *port)
+{
+ if (!port->peer)
+ return NULL;
+
+ /*
+ * TODO: decide on the format for the name, and perform sanity checks to
+ * prohibit nasty edge cases (e.g. embedded NULLs) if the format doesn't
+ * perform escaping itself.
+ *
+ * See also
+ * https://www.postgresql.org/message-id/daf119af-60a3-54d9-978e-8c97a602ca28%40dunslane.net
+ *
+ * TODO: allocate this string in an explicit context
+ * TODO: reconcile this with be_tls_get_peer_subject_name, which silently
+ * truncates if your buffer isn't big enough
+ */
+ return X509_NAME_oneline(X509_get_subject_name(port->peer), NULL, 0);
+}
+
void
be_tls_close(Port *port)
{
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 80c2672461..f74f7571ac 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2717,6 +2717,24 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
else
appendStringInfoString(buf, unpack_sql_state(edata->sqlerrcode));
break;
+ case 'Z':
+ if (MyProcPort)
+ {
+ const char *authn_id = MyProcPort->authn_id;
+
+ /* TODO: should we consider the empty ID as unknown too? */
+ if (authn_id == NULL)
+ authn_id = _("[unknown]");
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, authn_id);
+ else
+ appendStringInfoString(buf, authn_id);
+
+ }
+ else if (padding != 0)
+ appendStringInfoSpaces(buf,
+ padding > 0 ? padding : -padding);
+ break;
default:
/* format error - ignore it */
break;
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 66a8673d93..69ea0d5dfb 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,17 @@ typedef struct Port
*/
HbaLine *hba;
+ /*
+ * Authenticated identity. The meaning of this identifier is dependent on
+ * hba->auth_method; it's the identity (if any) that the user presented
+ * during the authentication cycle, before they were assigned a role. (It's
+ * the "left side" of a pg_ident usermap.)
+ *
+ * authn_id is NULL if the user has not actually been authenticated, for
+ * example if the "trust" auth method is in use.
+ */
+ const char *authn_id;
+
/*
* TCP keepalive and user timeout settings.
*
@@ -270,6 +281,7 @@ extern bool be_tls_get_compression(Port *port);
extern const char *be_tls_get_version(Port *port);
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 const char *be_tls_get_peer_full_subject_name(Port *port);
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);
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..8685d19487 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
if ($ENV{with_gssapi} eq 'yes')
{
- plan tests => 34;
+ plan tests => 43;
}
else
{
@@ -170,6 +170,7 @@ $node->append_conf(
'postgresql.conf', qq{
listen_addresses = '$hostaddr'
krb_server_keyfile = '$keytab'
+log_line_prefix = '%m [%p] %q%a (%u:%Z) '
logging_collector = on
log_connections = on
# these ensure stability of test results:
@@ -268,10 +269,18 @@ $node->restart;
test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '',
+ 'does not log unauthenticated principal',
+ "(test1:[unknown]) FATAL: GSSAPI authentication failed");
+
run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '',
+ 'logs authenticated principal even without authorized mapping',
+ "(test1:test1\@$realm) LOG: no match in usermap");
+
$node->append_conf('pg_ident.conf', qq{mymap /^(.*)\@$realm\$ \\1});
$node->restart;
@@ -304,6 +313,10 @@ test_access(
"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
);
+test_access($node, 'test1', 'SELECT true', 0, 'gssencmode=require',
+ 'logs authenticated principal for authorized connection',
+ "(test1:test1\@$realm) LOG: connection authorized:");
+
# Test that we can transport a reasonable amount of data.
test_query(
$node,
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..34a1e9a10d 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
if ($ENV{with_ldap} eq 'yes')
{
- plan tests => 22;
+ plan tests => 24;
}
else
{
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
my $node = get_new_node('node');
$node->init;
+$node->append_conf('postgresql.conf', "log_line_prefix = '%m [%p] %q%a (%u:%Z) '\n");
$node->start;
$node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
note "simple bind";
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf('pg_hba.conf',
qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -187,6 +191,22 @@ test_access($node, 'test1', 2,
$ENV{"PGPASSWORD"} = 'secret1';
test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+# make sure LDAP DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+ $log_contents,
+ qr/\(test1:uid=test1,dc=example,dc=net\).*SELECT 1/,
+ "authenticated DNs are logged");
+
+unlike(
+ $log_contents,
+ qr/\(test.:uid=test.,dc=example,dc=net\).*FATAL:.*LDAP authentication failed/,
+ "unauthenticated DNs are not logged");
+
+$node->start;
+
note "search+bind";
unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index fd2727b568..58f371a306 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,7 +13,7 @@ use SSLServer;
if ($ENV{with_openssl} eq 'yes')
{
- plan tests => 93;
+ plan tests => 96;
}
else
{
@@ -74,6 +74,10 @@ $node->start;
my $result = $node->safe_psql('postgres', "SHOW ssl_library");
is($result, 'OpenSSL', 'ssl_library parameter');
+$node->append_conf(
+ 'postgresql.conf',
+ "log_line_prefix = '%m [%p] %q%a (%u:%Z) '\n");
+
configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
'trust');
@@ -399,6 +403,9 @@ test_connect_fails(
qr/connection requires a valid client certificate/,
"certificate authorization fails without client cert");
+my $log = $node->rotate_logfile();
+$node->restart;
+
# correct client cert in unencrypted PEM
test_connect_ok(
$common_connstr,
@@ -406,6 +413,16 @@ test_connect_ok(
"certificate authorization succeeds with correct client cert in PEM format"
);
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+$node->start;
+
+like(
+ $log_contents,
+ qr/\(ssltestuser:\/CN=ssltestuser\).*SELECT \$\$connected/,
+ "authenticated DNs are logged");
+
# correct client cert in unencrypted DER
test_connect_ok(
$common_connstr,
@@ -499,6 +516,9 @@ test_connect_fails(
"certificate authorization fails with client cert belonging to another user"
);
+$log = $node->rotate_logfile();
+$node->restart;
+
# revoked client cert
test_connect_fails(
$common_connstr,
@@ -506,12 +526,24 @@ test_connect_fails(
qr/SSL error/,
"certificate authorization fails with revoked client cert");
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+like(
+ $log_contents,
+ qr/\(\[unknown\]:\[unknown\]\).*could not accept SSL connection: certificate verify failed/,
+ "unauthenticated DNs are not logged");
+
# Check that connecting with auth-option verify-full in pg_hba:
# works, iff username matches Common Name
# fails, iff username doesn't match Common Name.
$common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
+$log = $node->rotate_logfile();
+$node->restart;
+
test_connect_ok(
$common_connstr,
"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -533,6 +565,15 @@ test_connect_ok(
"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
);
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+unlike(
+ $log_contents,
+ qr/\(.*:\/CN=ssltestuser\)/,
+ "trust auth method does not set authenticated identity");
+
# intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index a088f71a1a..d8dd670c7a 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
my $supports_tls_server_end_point =
check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
@@ -38,6 +38,10 @@ note "setting up data directory";
my $node = get_new_node('primary');
$node->init;
+$node->append_conf(
+ 'postgresql.conf',
+ "log_line_prefix = '%m [%p] %q%a (%u:%Z) '\n");
+
# PGHOST is enforced here to set up the node, subsequent connections
# will use a dedicated connection string.
$ENV{PGHOST} = $node->host;
@@ -48,14 +52,42 @@ $node->start;
configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
"scram-sha-256", "pass", "scram-sha-256");
switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
$common_connstr =
"dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+ qr/password authentication failed/,
+ "Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+$log = $node->rotate_logfile();
+$node->start;
+
+unlike(
+ $log_contents,
+ qr/\(ssltestuser:ssltestuser\)/,
+ "SCRAM does not set authenticated identity with bad password");
+
# Default settings
+$ENV{PGPASSWORD} = "pass";
test_connect_ok($common_connstr, "user=ssltestuser",
"Basic SCRAM authentication with SSL");
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+like(
+ $log_contents,
+ qr/\(ssltestuser:ssltestuser\)/,
+ "Basic SCRAM sets the username as the authenticated identity");
+
# Test channel_binding
test_connect_fails(
$common_connstr,
@@ -102,6 +134,24 @@ test_connect_fails(
qr/channel binding required, but server authenticated client without channel binding/,
"Cert authentication and channel_binding=require");
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+ "sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+ "dbname=verifydb user=ssltestuser channel_binding=require",
+ "SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+like(
+ $log_contents,
+ qr/\(ssltestuser:ssltestuser\)/,
+ "SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
# clean up
unlink($client_tmp_key);
--
2.25.1