Support kerberos authentication for postgres_fdw
Hi hackers,
I'd like to add kerberos authentication support for postgres_fdw by adding two
options to user mapping: krb_client_keyfile and gssencmode.
In the backend we have krb_server_keyfile option to specify a keytab file to
be used by postgres server, krb_client_keyfile is doing mostly the same thing.
This allows postgres_fdw(backend process) to authenticate on behalf of a
logged in user who is querying the foreign table. The credential is kept in
the backend process memory instead of local file to prevent abuse by users
on the same host.
Because backend process is accessing the filesystem of the server host, this
option should only be manipulated by super user. Otherwise, normal user may
steal the identity or probe the server filesystem. This principal is the same to
sslcert and sslkey options in user mapping.
Thoughts?
Best regards,
Peifeng
Attachments:
v1-0001-kerberos-pgfdw.patchtext/x-patch; name=v1-0001-kerberos-pgfdw.patchDownload
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 82aa14a65d..ba2edf0cba 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -333,6 +333,38 @@ make_new_connection(ConnCacheEntry *entry, UserMapping *user)
entry->conn, server->servername, user->umid, user->userid);
}
+
+/*
+ * Look for "krb_client_keyfile" option in user mapping.
+ * If found, set the KRB5_CLIENT_KTNAME environment just like we do
+ * in backend. Also remove this option from option list.
+ *
+ * If there are multiple principals in the keytab file, only the
+ * default(first) one will be selected.
+ */
+static void setup_kerberos_env(UserMapping *user)
+{
+ ListCell *lc;
+ DefElem *d;
+ StringInfo si = NULL;
+ foreach(lc, user->options)
+ {
+ d = (DefElem *) lfirst(lc);
+ if (strcmp(d->defname, "krb_client_keyfile") == 0)
+ {
+ si = makeStringInfo();
+ appendStringInfo(si, "KRB5_CLIENT_KTNAME=%s", defGetString(d));
+ user->options = list_delete_cell(user->options, lc);
+ putenv(si->data);
+ /* We don't want credential to be cached as file, keep it within current process.
+ * The credential can be reused within the same session.
+ */
+ putenv("KRB5CCNAME=MEMORY:");
+ return;
+ }
+ }
+}
+
/*
* Connect to remote server using specified server and user mapping properties.
*/
@@ -350,6 +382,8 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
const char **values;
int n;
+ setup_kerberos_env(user);
+
/*
* Construct connection params from generic options of ForeignServer
* and UserMapping. (Some of them might not be libpq options, in
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b8561d6a3c..afb2dc9b14 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -188,7 +188,7 @@ ALTER USER MAPPING FOR public SERVER testserver1
ALTER USER MAPPING FOR public SERVER testserver1
OPTIONS (ADD sslmode 'require');
ERROR: invalid option "sslmode"
-HINT: Valid options in this context are: user, password, sslpassword, password_required, sslcert, sslkey
+HINT: Valid options in this context are: user, password, sslpassword, password_required, sslcert, sslkey, krb_client_keyfile, gssencmode
-- But we can add valid ones fine
ALTER USER MAPPING FOR public SERVER testserver1
OPTIONS (ADD sslpassword 'dummy');
@@ -196,6 +196,8 @@ ALTER USER MAPPING FOR public SERVER testserver1
-- permitted to check validation.
ALTER USER MAPPING FOR public SERVER testserver1
OPTIONS (ADD sslkey 'value', ADD sslcert 'value');
+ALTER USER MAPPING FOR public SERVER testserver1
+ OPTIONS (ADD krb_client_keyfile 'value', ADD gssencmode 'value');
ALTER FOREIGN TABLE ft1 OPTIONS (schema_name 'S 1', table_name 'T 1');
ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
@@ -9298,6 +9300,13 @@ HINT: User mappings with the sslcert or sslkey options set may only be created
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD sslkey 'foo.key');
ERROR: sslcert and sslkey are superuser-only
HINT: User mappings with the sslcert or sslkey options set may only be created or modified by the superuser
+-- unpriv user also cannot set krb_client_keyfile / gssencmode on the user mapping
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD krb_client_keyfile 'foo.ktab');
+ERROR: krb_client_keyfile and gssencmode are superuser-only
+HINT: User mappings with the krb_client_keyfile or gssencmode options set may only be created or modified by the superuser
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD gssencmode 'require');
+ERROR: krb_client_keyfile and gssencmode are superuser-only
+HINT: User mappings with the krb_client_keyfile or gssencmode options set may only be created or modified by the superuser
-- We're done with the role named after a specific user and need to check the
-- changes to the public mapping.
DROP USER MAPPING FOR CURRENT_USER SERVER loopback_nopw;
diff --git a/contrib/postgres_fdw/kerberos/.gitignore b/contrib/postgres_fdw/kerberos/.gitignore
new file mode 100644
index 0000000000..871e943d50
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/contrib/postgres_fdw/kerberos/Makefile b/contrib/postgres_fdw/kerberos/Makefile
new file mode 100644
index 0000000000..3efec922f0
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/Makefile
@@ -0,0 +1,25 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/kerberos
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/kerberos/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = contrib/postgres_fdw/kerberos
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export with_gssapi with_krb_srvnam
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/contrib/postgres_fdw/kerberos/README b/contrib/postgres_fdw/kerberos/README
new file mode 100644
index 0000000000..40b8758543
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/README
@@ -0,0 +1,45 @@
+contrib/postgres_fdw/kerberos/README
+
+Tests for Kerberos/GSSAPI functionality
+=======================================
+
+This directory contains a test suite for Kerberos/GSSAPI
+functionality. This requires a full MIT Kerberos installation,
+including server and client tools, and is therefore kept separate and
+not run by default.
+
+CAUTION: The test server run by this test is configured to listen for TCP
+connections on localhost. Any user on the same host is able to log in to the
+test server while the tests are running. Do not run this suite on a multi-user
+system where you don't trust all local users! Also, this test suite creates a
+KDC server that listens for TCP/IP connections on localhost without any real
+access control.
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+ make check
+or
+ make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested. With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster, as well as a test KDC server.
+
+Requirements
+============
+
+MIT Kerberos server and client tools are required. Heimdal is not
+supported.
+
+Debian/Ubuntu packages: krb5-admin-server krb5-kdc krb5-user
+
+RHEL/CentOS/Fedora packages: krb5-server krb5-workstation
+
+FreeBSD port: krb5 (base system has Heimdal)
diff --git a/contrib/postgres_fdw/kerberos/t/001_auth.pl b/contrib/postgres_fdw/kerberos/t/001_auth.pl
new file mode 100644
index 0000000000..156fe96f07
--- /dev/null
+++ b/contrib/postgres_fdw/kerberos/t/001_auth.pl
@@ -0,0 +1,471 @@
+
+# Copyright (c) 2021, PostgreSQL Global Development Group
+
+# Sets up a KDC and then runs a variety of tests to make sure that the
+# GSSAPI/Kerberos authentication and encryption are working properly,
+# that the options in pg_hba.conf and pg_ident.conf are handled correctly,
+# and that the server-side pg_stat_gssapi view reports what we expect to
+# see for each test.
+#
+# Since this requires setting up a full KDC, it doesn't make much sense
+# to have multiple test scripts (since they'd have to also create their
+# own KDC and that could cause race conditions or other problems)- so
+# just add whatever other tests are needed to here.
+#
+# See the README for additional information.
+
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Time::HiRes qw(usleep);
+use Cwd 'abs_path';
+
+if ($ENV{with_gssapi} eq 'yes')
+{
+ plan tests => 53;
+}
+else
+{
+ plan skip_all => 'GSSAPI/Kerberos not supported by this build';
+}
+
+my ($krb5_bin_dir, $krb5_sbin_dir);
+
+if ($^O eq 'darwin')
+{
+ $krb5_bin_dir = '/usr/local/opt/krb5/bin';
+ $krb5_sbin_dir = '/usr/local/opt/krb5/sbin';
+}
+elsif ($^O eq 'freebsd')
+{
+ $krb5_bin_dir = '/usr/local/bin';
+ $krb5_sbin_dir = '/usr/local/sbin';
+}
+elsif ($^O eq 'linux')
+{
+ $krb5_sbin_dir = '/usr/sbin';
+}
+
+my $krb5_config = 'krb5-config';
+my $kinit = 'kinit';
+my $kdb5_util = 'kdb5_util';
+my $kadmin_local = 'kadmin.local';
+my $krb5kdc = 'krb5kdc';
+
+if ($krb5_bin_dir && -d $krb5_bin_dir)
+{
+ $krb5_config = $krb5_bin_dir . '/' . $krb5_config;
+ $kinit = $krb5_bin_dir . '/' . $kinit;
+}
+if ($krb5_sbin_dir && -d $krb5_sbin_dir)
+{
+ $kdb5_util = $krb5_sbin_dir . '/' . $kdb5_util;
+ $kadmin_local = $krb5_sbin_dir . '/' . $kadmin_local;
+ $krb5kdc = $krb5_sbin_dir . '/' . $krb5kdc;
+}
+
+my $host = 'auth-test-localhost.postgresql.example.com';
+my $hostaddr = '127.0.0.1';
+my $realm = 'EXAMPLE.COM';
+
+
+my $tmp_check = abs_path(${TestLib::tmp_check});
+my $krb5_conf = "$tmp_check/krb5.conf";
+my $kdc_conf = "$tmp_check/kdc.conf";
+my $krb5_cache = "$tmp_check/krb5cc";
+my $krb5_log = "${TestLib::log_path}/krb5libs.log";
+my $kdc_log = "${TestLib::log_path}/krb5kdc.log";
+my $kdc_port = get_free_port();
+my $kdc_datadir = "$tmp_check/krb5kdc";
+my $kdc_pidfile = "$tmp_check/krb5kdc.pid";
+my $keytab = "$tmp_check/krb5.keytab";
+
+my $dbname = 'testdb';
+my $testuser = 'testuser';
+my $test1 = 'test1';
+my $application = '001_auth.pl';
+
+note "setting up Kerberos";
+
+my ($stdout, $krb5_version);
+run_log [ $krb5_config, '--version' ], '>', \$stdout
+ or BAIL_OUT("could not execute krb5-config");
+BAIL_OUT("Heimdal is not supported") if $stdout =~ m/heimdal/;
+$stdout =~ m/Kerberos 5 release ([0-9]+\.[0-9]+)/
+ or BAIL_OUT("could not get Kerberos version");
+$krb5_version = $1;
+
+append_to_file(
+ $krb5_conf,
+ qq![logging]
+default = FILE:$krb5_log
+kdc = FILE:$kdc_log
+
+[libdefaults]
+default_realm = $realm
+
+[realms]
+$realm = {
+ kdc = $hostaddr:$kdc_port
+}!);
+
+append_to_file(
+ $kdc_conf,
+ qq![kdcdefaults]
+!);
+
+# For new-enough versions of krb5, use the _listen settings rather
+# than the _ports settings so that we can bind to localhost only.
+if ($krb5_version >= 1.15)
+{
+ append_to_file(
+ $kdc_conf,
+ qq!kdc_listen = $hostaddr:$kdc_port
+kdc_tcp_listen = $hostaddr:$kdc_port
+!);
+}
+else
+{
+ append_to_file(
+ $kdc_conf,
+ qq!kdc_ports = $kdc_port
+kdc_tcp_ports = $kdc_port
+!);
+}
+
+append_to_file(
+ $kdc_conf,
+ qq!
+[realms]
+$realm = {
+ database_name = $kdc_datadir/principal
+ admin_keytab = FILE:$kdc_datadir/kadm5.keytab
+ acl_file = $kdc_datadir/kadm5.acl
+ key_stash_file = $kdc_datadir/_k5.$realm
+}!);
+
+mkdir $kdc_datadir or die;
+
+# Ensure that we use test's config and cache files, not global ones.
+$ENV{'KRB5_CONFIG'} = $krb5_conf;
+$ENV{'KRB5_KDC_PROFILE'} = $kdc_conf;
+$ENV{'KRB5CCNAME'} = $krb5_cache;
+
+my $service_principal = "$ENV{with_krb_srvnam}/$host";
+
+system_or_bail $kdb5_util, 'create', '-s', '-P', 'secret0';
+
+my $test1_password = 'secret1';
+system_or_bail $kadmin_local, '-q', "addprinc -randkey $testuser";
+system_or_bail $kadmin_local, '-q', "addprinc -randkey $service_principal";
+system_or_bail $kadmin_local, '-q', "ktadd -k $keytab $testuser";
+system_or_bail $kadmin_local, '-q', "ktadd -k $keytab $service_principal";
+
+system_or_bail $krb5kdc, '-P', $kdc_pidfile;
+
+#system_or_bail $krb5kdc;
+
+END
+{
+ kill 'INT', `cat $kdc_pidfile` if -f $kdc_pidfile;
+}
+
+note "setting up PostgreSQL instance";
+
+# server node for fdw to connect
+my $server_node = get_new_node('node');
+$server_node->init;
+$server_node->append_conf(
+ 'postgresql.conf', qq{
+listen_addresses = '$hostaddr'
+krb_server_keyfile = '$keytab'
+log_connections = on
+lc_messages = 'C'
+});
+$server_node->start;
+
+note "setting up user on server";
+$server_node->safe_psql('postgres', "CREATE USER $testuser;");
+# there's no direct way to get the backend pid from postgres_fdw, create a VIEW at server side to provide this.
+$server_node->safe_psql('postgres', 'CREATE VIEW my_pg_stat_gssapi AS
+ SELECT S.pid,
+ S.gss_auth AS gss_authenticated,
+ S.gss_princ AS principal,
+ S.gss_enc AS encrypted
+ FROM pg_stat_get_activity(NULL) AS S
+ WHERE S.client_port IS NOT NULL AND S.pid = pg_backend_pid();');
+$server_node->safe_psql('postgres', "GRANT ALL ON my_pg_stat_gssapi TO $testuser;");
+
+my $server_node_port = $server_node->port;
+note "setting up PostgreSQL fdw instance";
+# client node that runs postgres fdw
+my $fdw_node = get_new_node('node_fdw');
+$fdw_node->init;
+$fdw_node->append_conf(
+ 'postgresql.conf', qq{
+listen_addresses = '$hostaddr'
+log_connections = on
+lc_messages = 'C'
+});
+$fdw_node->start;
+
+$fdw_node->safe_psql('postgres', "CREATE USER $testuser;");
+$fdw_node->safe_psql('postgres', "CREATE DATABASE $dbname;");
+$fdw_node->safe_psql($dbname, 'CREATE EXTENSION postgres_fdw');
+$fdw_node->safe_psql($dbname, "CREATE SERVER postgres_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$host', hostaddr '$hostaddr', port '$server_node_port', dbname 'postgres');");
+$fdw_node->safe_psql($dbname, "CREATE FOREIGN TABLE my_pg_stat_gssapi\(pid int, gss_authenticated boolean, principal text, encrypted boolean\) SERVER postgres_server OPTIONS(table_name 'my_pg_stat_gssapi'); ");
+$fdw_node->safe_psql($dbname, "CREATE USER MAPPING for $testuser SERVER postgres_server OPTIONS (user '$testuser');");
+$fdw_node->safe_psql($dbname, "GRANT SELECT ON my_pg_stat_gssapi TO $testuser;");
+
+note "running tests";
+
+sub set_user_mapping_option
+{
+ my ($option, $value, $action) = @_;
+
+ if (!defined $action)
+ {
+ $action = "SET";
+ }
+
+ my $option_str = "$option";
+ if (defined $value)
+ {
+ $option_str = "$option '$value'";
+ }
+
+ $fdw_node->safe_psql($dbname, "ALTER USER MAPPING for $testuser SERVER postgres_server OPTIONS ($action $option_str);");
+}
+
+sub set_gssencmode
+{
+ my ($mode, $action) = @_;
+ set_user_mapping_option('gssencmode', $mode, $action);
+}
+
+# Test connection success or failure, and if success, that query returns true.
+sub test_access
+{
+ my ($query, $expected_res, $test_name,
+ $expect_output, @expect_log_msgs)
+ = @_;
+
+ my %params = (sql => $query,);
+
+ # Check log in server node. Obtain log file size before we run any query.
+ # This way we can obtain logs for this psql connection only.
+ my $log_location = -s $server_node->logfile;
+
+ # Run psql in fdw node.
+ my ($ret, $stdout, $stderr) = $fdw_node->psql(
+ $dbname,
+ $query,
+ extra_params => ['-w'],
+ connstr => "user=$testuser dbname=$dbname host=$host hostaddr=$hostaddr");
+
+ is($ret, $expected_res, $test_name);
+
+ if (defined $expect_output)
+ {
+ if ($expected_res eq 0)
+ {
+ like($stdout, $expect_output, "$test_name: result matches");
+ }
+ else
+ {
+ like($stderr, $expect_output, "$test_name: result matches");
+ }
+ }
+
+ if (@expect_log_msgs)
+ {
+ # Match every message literally.
+ my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+ my $log_contents = TestLib::slurp_file($server_node->logfile, $log_location);
+
+ while (my $regex = shift @regexes)
+ {
+ like($log_contents, $regex, "$test_name: matches");
+ }
+ }
+}
+
+test_access(
+ 'SELECT * FROM pg_user_mapping',
+ 3,
+ 'no access to user mapping',
+ '/permission denied for table pg_user_mapping/'
+);
+
+test_access(
+ 'SELECT pid FROM my_pg_stat_gssapi',
+ 3,
+ 'fails without password',
+ '/Non-superusers must provide a password in the user mapping/'
+);
+
+set_user_mapping_option('password_required', 'false', 'ADD');
+
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+ qq{host all all $hostaddr/32 gss map=mymap});
+$server_node->restart;
+
+test_access(
+ 'SELECT pid FROM my_pg_stat_gssapi',
+ 3,
+ 'fails without ticket',
+ '/No Kerberos credentials available/'
+);
+
+my $keytabfile = abs_path($keytab);
+$fdw_node->safe_psql($dbname, "ALTER USER MAPPING for $testuser SERVER postgres_server OPTIONS (ADD krb_client_keyfile '$keytabfile');");
+test_access(
+ 'SELECT true from my_pg_stat_gssapi',
+ 3,
+ 'fails without mapping',
+ "/GSSAPI authentication failed for user \"$testuser\"/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "no match in usermap \"mymap\" for user \"$testuser\""
+);
+
+$server_node->append_conf('pg_ident.conf', qq{mymap /^(.*)\@$realm\$ \\1});
+$server_node->restart;
+
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+ 0,
+ 'succeeds with mapping with default gssencmode and host hba',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+# set gssencmode in user mapping
+set_gssencmode('prefer', 'ADD');
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+ 0,
+ 'succeeds with GSS-encrypted access preferred with host hba',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('require');
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+ 0,
+ 'succeeds with GSS-encrypted access required with host hba',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+
+# Test that we can transport a reasonable amount of data.
+test_access(
+ 'SELECT generate_series(1, 100000) FROM my_pg_stat_gssapi;',
+ 0,
+ 'receiving 100K lines works',
+ qr/^1\n.*\n1024\n.*\n9999\n.*\n100000$/s
+);
+
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+ qq{hostgssenc all all $hostaddr/32 gss map=mymap});
+$server_node->restart;
+
+set_gssencmode('prefer');
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi',
+ 0,
+ 'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('require');
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi',
+ 0,
+ 'succeeds with GSS-encrypted access required and hostgssenc hba',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('disable');
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi',
+ 3,
+ 'fails with GSS encryption disabled and hostgssenc hba',
+ qr/\Qno pg_hba.conf entry for host "$hostaddr", user "$testuser", database "postgres", no encryption\E/,
+ "no pg_hba.conf entry for host \"$hostaddr\", user \"$testuser\", database \"postgres\", no encryption");
+
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+ qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
+$server_node->restart;
+
+set_gssencmode('prefer');
+test_access(
+ 'SELECT gss_authenticated and not encrypted from my_pg_stat_gssapi',
+ 0,
+ 'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=no, principal=$testuser\@$realm)"
+);
+
+set_gssencmode('require');
+test_access(
+ 'SELECT gss_authenticated and not encrypted from my_pg_stat_gssapi',
+ 3,
+ 'fails with GSS-encrypted access required and hostnogssenc hba',
+ qr/\Qno pg_hba.conf entry for host "$hostaddr", user "$testuser", database "postgres", GSS encryption\E/,
+ "no pg_hba.conf entry for host \"$hostaddr\", user \"$testuser\", database \"postgres\", GSS encryption");
+
+set_gssencmode('disable');
+test_access(
+ 'SELECT gss_authenticated and not encrypted from my_pg_stat_gssapi;',
+ 0,
+ 'succeeds with GSS encryption disabled and hostnogssenc hba',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=no, principal=$testuser\@$realm)"
+);
+
+
+truncate($server_node->data_dir . '/pg_ident.conf', 0);
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+ qq{host all all $hostaddr/32 gss include_realm=0});
+$server_node->restart;
+
+set_gssencmode(undef, 'DROP');
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+ 0,
+ 'succeeds with include_realm=0 and defaults',
+ "/^t\$/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss",
+ "connection authorized: user=$testuser database=postgres application_name=postgres_fdw GSS (authenticated=yes, encrypted=yes, principal=$testuser\@$realm)"
+);
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($server_node->data_dir . '/pg_hba.conf');
+$server_node->append_conf('pg_hba.conf',
+ qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$server_node->restart;
+
+test_access(
+ 'SELECT gss_authenticated AND encrypted from my_pg_stat_gssapi;',
+ 3,
+ 'fails with wrong krb_realm, but still authenticates',
+ "/GSSAPI authentication failed for user \"$testuser\"/",
+ "connection authenticated: identity=\"$testuser\@$realm\" method=gss");
\ No newline at end of file
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 4593cbc540..4326f95846 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -193,6 +193,16 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
errmsg("sslcert and sslkey are superuser-only"),
errhint("User mappings with the sslcert or sslkey options set may only be created or modified by the superuser")));
}
+ else if (strcmp(def->defname, "krb_client_keyfile") == 0 ||
+ strcmp(def->defname, "gssencmode") == 0)
+ {
+ /* similarly for krb_client_keyfile / gssencmode on user mapping */
+ if (catalog == UserMappingRelationId && !superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("krb_client_keyfile and gssencmode are superuser-only"),
+ errhint("User mappings with the krb_client_keyfile or gssencmode options set may only be created or modified by the superuser")));
+ }
}
PG_RETURN_VOID();
@@ -247,6 +257,10 @@ InitPgFdwOptions(void)
{"sslcert", UserMappingRelationId, true},
{"sslkey", UserMappingRelationId, true},
+ /* kerberos settings */
+ {"krb_client_keyfile", UserMappingRelationId, false},
+ {"gssencmode", UserMappingRelationId, false},
+
{NULL, InvalidOid, false}
};
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index c283e74715..9209c32681 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -211,6 +211,8 @@ ALTER USER MAPPING FOR public SERVER testserver1
-- permitted to check validation.
ALTER USER MAPPING FOR public SERVER testserver1
OPTIONS (ADD sslkey 'value', ADD sslcert 'value');
+ALTER USER MAPPING FOR public SERVER testserver1
+ OPTIONS (ADD krb_client_keyfile 'value', ADD gssencmode 'value');
ALTER FOREIGN TABLE ft1 OPTIONS (schema_name 'S 1', table_name 'T 1');
ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
@@ -2793,6 +2795,9 @@ SELECT 1 FROM ft1_nopw LIMIT 1;
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (SET password_required 'true');
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD sslcert 'foo.crt');
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD sslkey 'foo.key');
+-- unpriv user also cannot set krb_client_keyfile / gssencmode on the user mapping
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD krb_client_keyfile 'foo.ktab');
+ALTER USER MAPPING FOR CURRENT_USER SERVER loopback_nopw OPTIONS (ADD gssencmode 'require');
-- We're done with the role named after a specific user and need to check the
-- changes to the public mapping.
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index c9fce77599..c46e63ae82 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -144,6 +144,15 @@
connection setting.
</para>
</listitem>
+ <listitem>
+ <para>
+ <literal>krb_client_keyfile</literal> location of keytab
+ file on fdw server host. Only the first principal in this keytab can
+ be selected. It will only be cached in backend memory to prevent other
+ users from using the ticket.
+ <literal>gssencmode</literal> same to "gssencmode" libpq option.
+ </para>
+ </listitem>
</itemizedlist>
</para>
Peifeng Qiu <peifengq@vmware.com> writes:
I'd like to add kerberos authentication support for postgres_fdw by adding two
options to user mapping: krb_client_keyfile and gssencmode.
As you note, this'd have to be restricted to superusers, which makes it
seem like a pretty bad idea. We really don't want to be in a situation
of pushing people to run day-to-day stuff as superuser. Yeah, having
access to kerberos auth sounds good on the surface, but it seems like
it would be a net loss in security because of that.
Is there some other way?
regards, tom lane
On Fri, Jul 9, 2021 at 3:49 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Peifeng Qiu <peifengq@vmware.com> writes:
I'd like to add kerberos authentication support for postgres_fdw by adding two
options to user mapping: krb_client_keyfile and gssencmode.As you note, this'd have to be restricted to superusers, which makes it
seem like a pretty bad idea. We really don't want to be in a situation
of pushing people to run day-to-day stuff as superuser. Yeah, having
access to kerberos auth sounds good on the surface, but it seems like
it would be a net loss in security because of that.Is there some other way?
ISTM the right way to do this would be using Kerberos delegation. That
is, the system would be set up so that the postgres service principal
is trusted for kerberos delegation and it would then pass through the
actual Kerberos authentication from the client.
At least at a quick glance this does not look like what this patch is
doing, sadly.
What does kerberos auth with a fixed key on the client (being the
postgres server in this auth step) actually help with?
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Sorry I have sent a duplicate email. I will first continue discussion
in the other thread and then submit it after we have a conclusion.
Thanks.
Peifeng
Import Notes
Resolved by subject fallback
As you note, this'd have to be restricted to superusers, which makes it
seem like a pretty bad idea. We really don't want to be in a situation
of pushing people to run day-to-day stuff as superuser. Yeah, having
access to kerberos auth sounds good on the surface, but it seems like
it would be a net loss in security because of that.
I can imagine the use case would be a superuser creates the user
mapping and foreign table, then grants access of foreign table to
a normal user. This way the normal user can execute queries on the
foreign table but can't access sensitive information in user mapping.
The main purpose of this patch is to provide a simple way to do
kerberos authentication with the least modification possible.
ISTM the right way to do this would be using Kerberos delegation. That
is, the system would be set up so that the postgres service principal
is trusted for kerberos delegation and it would then pass through the
actual Kerberos authentication from the client.
I agree this sounds like the ideal solution. If I understand it correctly,
this approach requires both postgres servers to use same kerberos
settings(kdc, realm, etc), and the FDW server can just "forward"
necessary information to authenticate on behalf of the same user.
I will spend some time to investigate it and reach out later.
Best regards,
Peifeng
On Mon, Jul 12, 2021 at 5:43 AM Peifeng Qiu <peifengq@vmware.com> wrote:
As you note, this'd have to be restricted to superusers, which makes it
seem like a pretty bad idea. We really don't want to be in a situation
of pushing people to run day-to-day stuff as superuser. Yeah, having
access to kerberos auth sounds good on the surface, but it seems like
it would be a net loss in security because of that.I can imagine the use case would be a superuser creates the user
mapping and foreign table, then grants access of foreign table to
a normal user. This way the normal user can execute queries on the
foreign table but can't access sensitive information in user mapping.The main purpose of this patch is to provide a simple way to do
kerberos authentication with the least modification possible.
But in this case, what dose Kerberos give over just using a password
based solution? It adds complexity, but what's teh actual gain?
ISTM the right way to do this would be using Kerberos delegation. That
is, the system would be set up so that the postgres service principal
is trusted for kerberos delegation and it would then pass through the
actual Kerberos authentication from the client.I agree this sounds like the ideal solution. If I understand it correctly,
this approach requires both postgres servers to use same kerberos
settings(kdc, realm, etc), and the FDW server can just "forward"
necessary information to authenticate on behalf of the same user.
I will spend some time to investigate it and reach out later.
I don't actually know if they have to be in the same realm, I *think*
kerberos delegations work across trusted realms, but I'm not sure
about that.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
But in this case, what dose Kerberos give over just using a password
based solution? It adds complexity, but what's teh actual gain?
That's due to policy of some customers. They require all login to be kerberos
based and password-less. I suppose this way they don't need to maintain
passwords in each database and the same keytab file may be used in
connections to multiple databases.
If we can do the delegation approach right, it's clearly a superior solution
since keytab file management is also quite heavy burden.
Greetings,
* Magnus Hagander (magnus@hagander.net) wrote:
On Mon, Jul 12, 2021 at 5:43 AM Peifeng Qiu <peifengq@vmware.com> wrote:
As you note, this'd have to be restricted to superusers, which makes it
seem like a pretty bad idea. We really don't want to be in a situation
of pushing people to run day-to-day stuff as superuser. Yeah, having
access to kerberos auth sounds good on the surface, but it seems like
it would be a net loss in security because of that.I can imagine the use case would be a superuser creates the user
mapping and foreign table, then grants access of foreign table to
a normal user. This way the normal user can execute queries on the
foreign table but can't access sensitive information in user mapping.The main purpose of this patch is to provide a simple way to do
kerberos authentication with the least modification possible.But in this case, what dose Kerberos give over just using a password
based solution? It adds complexity, but what's teh actual gain?
This is a bad idea.
ISTM the right way to do this would be using Kerberos delegation. That
is, the system would be set up so that the postgres service principal
is trusted for kerberos delegation and it would then pass through the
actual Kerberos authentication from the client.I agree this sounds like the ideal solution. If I understand it correctly,
this approach requires both postgres servers to use same kerberos
settings(kdc, realm, etc), and the FDW server can just "forward"
necessary information to authenticate on behalf of the same user.
I will spend some time to investigate it and reach out later.I don't actually know if they have to be in the same realm, I *think*
kerberos delegations work across trusted realms, but I'm not sure
about that.
This is a good idea, and yes, delegation works just fine across realms
if the environment is properly set up for cross-realm trust.
Kerberos delegation is absolutely the way to go here. I don't think we
should even be thinking of accepting something that requires users to
put a bunch of keytab files on the PG server to allow that server to
reach out to other servers...
I'd be happy to work with someone on an effort to support Kerberos
delegated credentials; it's been something that I've wanted to work on
for a long time.
Thanks,
Stephen
Hi all.
I've come up with a proof-of-concept patch using the delegation/proxy approach.
Let's say we have two DB, one for FDW and one for the real server. When client
connects to FDW server using kerberos authentication, we can obtain a "proxy"
credential and store it in the global variable "MyProcPort->gss->proxy". This can
be then passed to gssapi calls during libpq kerberos setup when the foreign table
is queried.
This will mitigate the need for keytab file on FDW server. We will also have to
relax the password requirement for user mapping.
The big problem here is how to pass proxy credential from backend to libpq-fe
safely. Because libpq called in postgres_fdw is compiled as frontend binary, we'd
better not include any backend related stuff in libpq-fe.
In this patch I use a very ugly hack to work around this. First take pointer address
of the variable MyProcPort->gss->proxy, convert it to hex string, and then pass
it as libpq option "gss_proxy_cred". Any idea about how to do this in a more
elegant way?
Best regards,
Peifeng
Attachments:
v2-0001-kerberos-pgfdw.patchtext/x-patch; name=v2-0001-kerberos-pgfdw.patchDownload
diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile
index c1b0cad453..2113fa8028 100644
--- a/contrib/postgres_fdw/Makefile
+++ b/contrib/postgres_fdw/Makefile
@@ -7,7 +7,8 @@ OBJS = \
deparse.o \
option.o \
postgres_fdw.o \
- shippable.o
+ shippable.o \
+ gss_proxy.o
PGFILEDESC = "postgres_fdw - foreign data wrapper for PostgreSQL"
PG_CPPFLAGS = -I$(libpq_srcdir)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 82aa14a65d..e5e5f534ae 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -333,6 +333,9 @@ make_new_connection(ConnCacheEntry *entry, UserMapping *user)
entry->conn, server->servername, user->umid, user->userid);
}
+extern char* get_gss_proxy_cred();
+extern bool has_gss_proxy_cred();
+
/*
* Connect to remote server using specified server and user mapping properties.
*/
@@ -349,14 +352,16 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
const char **keywords;
const char **values;
int n;
+ char *gss_proxy_cred_addr;
/*
* Construct connection params from generic options of ForeignServer
* and UserMapping. (Some of them might not be libpq options, in
- * which case we'll just waste a few array slots.) Add 3 extra slots
- * for fallback_application_name, client_encoding, end marker.
+ * which case we'll just waste a few array slots.) Add 4 extra slots
+ * for fallback_application_name, client_encoding, end marker,
+ * gss_proxy_cred.
*/
- n = list_length(server->options) + list_length(user->options) + 3;
+ n = list_length(server->options) + list_length(user->options) + 4;
keywords = (const char **) palloc(n * sizeof(char *));
values = (const char **) palloc(n * sizeof(char *));
@@ -376,6 +381,14 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
values[n] = GetDatabaseEncodingName();
n++;
+ gss_proxy_cred_addr = get_gss_proxy_cred();
+ if (gss_proxy_cred_addr != NULL)
+ {
+ keywords[n] = "gss_proxy_cred";
+ values[n] = gss_proxy_cred_addr;
+ n++;
+ }
+
keywords[n] = values[n] = NULL;
/* verify the set of connection parameters */
@@ -476,6 +489,9 @@ UserMappingPasswordRequired(UserMapping *user)
{
ListCell *cell;
+ if (has_gss_proxy_cred())
+ return false;
+
foreach(cell, user->options)
{
DefElem *def = (DefElem *) lfirst(cell);
diff --git a/contrib/postgres_fdw/gss_proxy.c b/contrib/postgres_fdw/gss_proxy.c
new file mode 100644
index 0000000000..1f016c54fc
--- /dev/null
+++ b/contrib/postgres_fdw/gss_proxy.c
@@ -0,0 +1,25 @@
+#include <gssapi/gssapi.h>
+
+#include "postgres.h"
+#include "miscadmin.h"
+#include "libpq/libpq-be.h"
+
+bool has_gss_proxy_cred()
+{
+ return MyProcPort->gss != NULL && MyProcPort->gss->proxy != NULL;
+}
+
+/* ugly hack to pass the gss proxy credential from backend Port to frontend libpq. */
+char* get_gss_proxy_cred()
+{
+ char* addr = NULL;
+ if (MyProcPort->gss)
+ {
+ if (MyProcPort->gss->proxy)
+ {
+ addr = palloc(sizeof(void*) + 3);
+ sprintf(addr, "%p", MyProcPort->gss->proxy);
+ }
+ }
+ return addr;
+}
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 8cc23ef7fb..2cb3e3b435 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -935,12 +935,17 @@ pg_GSS_recvauth(Port *port)
}
/*
- * We accept any service principal that's present in our keytab. This
- * increases interoperability between kerberos implementations that see
- * for example case sensitivity differently, while not really opening up
- * any vector of attack.
+ * Acquire default credential with GSS_C_BOTH. Comprare to using
+ * GSS_C_NO_CREDENTIAL, this allows us to also acquire a proxy
+ * credential, which can be used in postgres_fdw as for delegation.
*/
- port->gss->cred = GSS_C_NO_CREDENTIAL;
+ maj_stat = gss_acquire_cred(&min_stat, GSS_C_NO_NAME, 0,
+ NULL, GSS_C_BOTH, &port->gss->cred, NULL, NULL);
+ if (maj_stat != GSS_S_COMPLETE)
+ {
+ pg_GSS_error(_("gss_acquire_cred failed"), maj_stat, min_stat);
+ return STATUS_ERROR;
+ }
/*
* Initialize sequence with an empty context
@@ -997,7 +1002,7 @@ pg_GSS_recvauth(Port *port)
&port->gss->outbuf,
&gflags,
NULL,
- NULL);
+ &port->gss->proxy);
/* gbuf no longer used */
pfree(buf.data);
diff --git a/src/backend/libpq/be-secure-gssapi.c b/src/backend/libpq/be-secure-gssapi.c
index 316ca65db5..4a130f8d0f 100644
--- a/src/backend/libpq/be-secure-gssapi.c
+++ b/src/backend/libpq/be-secure-gssapi.c
@@ -536,6 +536,19 @@ secure_open_gssapi(Port *port)
}
}
+ /*
+ * Acquire default credential with GSS_C_BOTH. Comprare to using
+ * GSS_C_NO_CREDENTIAL, this allows us to also acquire a proxy
+ * credential, which can be used in postgres_fdw as for delegation.
+ */
+ major = gss_acquire_cred(&minor, GSS_C_NO_NAME, 0,
+ NULL, GSS_C_BOTH, &port->gss->cred, NULL, NULL);
+ if (major != GSS_S_COMPLETE)
+ {
+ pg_GSS_error(_("gss_acquire_cred failed"), major, minor);
+ return -1;
+ }
+
while (true)
{
ssize_t ret;
@@ -585,10 +598,10 @@ secure_open_gssapi(Port *port)
/* Process incoming data. (The client sends first.) */
major = gss_accept_sec_context(&minor, &port->gss->ctx,
- GSS_C_NO_CREDENTIAL, &input,
+ port->gss->cred, &input,
GSS_C_NO_CHANNEL_BINDINGS,
&port->gss->name, NULL, &output, NULL,
- NULL, NULL);
+ NULL, &port->gss->proxy);
if (GSS_ERROR(major))
{
pg_GSS_error(_("could not accept GSSAPI security context"),
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..6efaf75bfd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -95,6 +95,8 @@ typedef struct
* GSSAPI auth was not used */
bool auth; /* GSSAPI Authentication used */
bool enc; /* GSSAPI encryption in use */
+
+ gss_cred_id_t proxy;
#endif
} pg_gssinfo;
#endif
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 3421ed4685..70da371366 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -62,6 +62,7 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
lmin_s;
gss_buffer_desc ginbuf;
gss_buffer_desc goutbuf;
+ gss_cred_id_t proxy;
/*
* On first call, there's no input token. On subsequent calls, read the
@@ -93,9 +94,15 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
ginbuf.length = 0;
ginbuf.value = NULL;
}
+ proxy = GSS_C_NO_CREDENTIAL;
+ if (conn->gssproxycred)
+ {
+ if (1 != sscanf(conn->gssproxycred, "%p", &proxy))
+ proxy = GSS_C_NO_CREDENTIAL;
+ }
maj_stat = gss_init_sec_context(&min_stat,
- GSS_C_NO_CREDENTIAL,
+ proxy,
&conn->gctx,
conn->gtarg_nam,
GSS_C_NO_OID,
@@ -1307,4 +1314,4 @@ PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
libpq_gettext("out of memory\n"));
return crypt_pwd;
-}
+}
\ No newline at end of file
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index e950b41374..25ac862dc6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -335,6 +335,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"GSS-library", "", 7, /* sizeof("gssapi") == 7 */
offsetof(struct pg_conn, gsslib)},
+ {"gss_proxy_cred", "PGGSSPROXYCRED", NULL, NULL,
+ "GSS proxy credential", "", 19, /* sizeof("0x0123456789abcdef") */
+ offsetof(struct pg_conn, gssproxycred)},
+
{"replication", NULL, NULL, NULL,
"Replication", "D", 5,
offsetof(struct pg_conn, replication)},
@@ -4116,6 +4120,8 @@ freePGconn(PGconn *conn)
free(conn->krbsrvname);
if (conn->gsslib)
free(conn->gsslib);
+ if (conn->gssproxycred)
+ free(conn->gssproxycred);
if (conn->connip)
free(conn->connip);
/* Note that conn->Pfdebug is not ours to close or free */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e9f214b61b..120d13f03f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -390,6 +390,7 @@ struct pg_conn
char *krbsrvname; /* Kerberos service name */
char *gsslib; /* What GSS library to use ("gssapi" or
* "sspi") */
+ char *gssproxycred; /* proxy/delegation credential address(hex string) */
char *ssl_min_protocol_version; /* minimum TLS protocol version */
char *ssl_max_protocol_version; /* maximum TLS protocol version */
char *target_session_attrs; /* desired session properties */
Import Notes
Resolved by subject fallback