1# Sets up a KDC and then runs a variety of tests to make sure that the 2# GSSAPI/Kerberos authentication and encryption are working properly, 3# that the options in pg_hba.conf and pg_ident.conf are handled correctly, 4# and that the server-side pg_stat_gssapi view reports what we expect to 5# see for each test. 6# 7# Since this requires setting up a full KDC, it doesn't make much sense 8# to have multiple test scripts (since they'd have to also create their 9# own KDC and that could cause race conditions or other problems)- so 10# just add whatever other tests are needed to here. 11# 12# See the README for additional information. 13 14use strict; 15use warnings; 16use TestLib; 17use PostgresNode; 18use Test::More; 19 20if ($ENV{with_gssapi} eq 'yes') 21{ 22 plan tests => 18; 23} 24else 25{ 26 plan skip_all => 'GSSAPI/Kerberos not supported by this build'; 27} 28 29my ($krb5_bin_dir, $krb5_sbin_dir); 30 31if ($^O eq 'darwin') 32{ 33 $krb5_bin_dir = '/usr/local/opt/krb5/bin'; 34 $krb5_sbin_dir = '/usr/local/opt/krb5/sbin'; 35} 36elsif ($^O eq 'freebsd') 37{ 38 $krb5_bin_dir = '/usr/local/bin'; 39 $krb5_sbin_dir = '/usr/local/sbin'; 40} 41elsif ($^O eq 'linux') 42{ 43 $krb5_sbin_dir = '/usr/sbin'; 44} 45 46my $krb5_config = 'krb5-config'; 47my $kinit = 'kinit'; 48my $kdb5_util = 'kdb5_util'; 49my $kadmin_local = 'kadmin.local'; 50my $krb5kdc = 'krb5kdc'; 51 52if ($krb5_bin_dir && -d $krb5_bin_dir) 53{ 54 $krb5_config = $krb5_bin_dir . '/' . $krb5_config; 55 $kinit = $krb5_bin_dir . '/' . $kinit; 56} 57if ($krb5_sbin_dir && -d $krb5_sbin_dir) 58{ 59 $kdb5_util = $krb5_sbin_dir . '/' . $kdb5_util; 60 $kadmin_local = $krb5_sbin_dir . '/' . $kadmin_local; 61 $krb5kdc = $krb5_sbin_dir . '/' . $krb5kdc; 62} 63 64my $host = 'auth-test-localhost.postgresql.example.com'; 65my $hostaddr = '127.0.0.1'; 66my $realm = 'EXAMPLE.COM'; 67 68my $krb5_conf = "${TestLib::tmp_check}/krb5.conf"; 69my $kdc_conf = "${TestLib::tmp_check}/kdc.conf"; 70my $krb5_cache = "${TestLib::tmp_check}/krb5cc"; 71my $krb5_log = "${TestLib::log_path}/krb5libs.log"; 72my $kdc_log = "${TestLib::log_path}/krb5kdc.log"; 73my $kdc_port = get_free_port(); 74my $kdc_datadir = "${TestLib::tmp_check}/krb5kdc"; 75my $kdc_pidfile = "${TestLib::tmp_check}/krb5kdc.pid"; 76my $keytab = "${TestLib::tmp_check}/krb5.keytab"; 77 78note "setting up Kerberos"; 79 80my ($stdout, $krb5_version); 81run_log [ $krb5_config, '--version' ], '>', \$stdout 82 or BAIL_OUT("could not execute krb5-config"); 83BAIL_OUT("Heimdal is not supported") if $stdout =~ m/heimdal/; 84$stdout =~ m/Kerberos 5 release ([0-9]+\.[0-9]+)/ 85 or BAIL_OUT("could not get Kerberos version"); 86$krb5_version = $1; 87 88append_to_file( 89 $krb5_conf, 90 qq![logging] 91default = FILE:$krb5_log 92kdc = FILE:$kdc_log 93 94[libdefaults] 95default_realm = $realm 96 97[realms] 98$realm = { 99 kdc = $hostaddr:$kdc_port 100}!); 101 102append_to_file( 103 $kdc_conf, 104 qq![kdcdefaults] 105!); 106 107# For new-enough versions of krb5, use the _listen settings rather 108# than the _ports settings so that we can bind to localhost only. 109if ($krb5_version >= 1.15) 110{ 111 append_to_file( 112 $kdc_conf, 113 qq!kdc_listen = $hostaddr:$kdc_port 114kdc_tcp_listen = $hostaddr:$kdc_port 115!); 116} 117else 118{ 119 append_to_file( 120 $kdc_conf, 121 qq!kdc_ports = $kdc_port 122kdc_tcp_ports = $kdc_port 123!); 124} 125append_to_file( 126 $kdc_conf, 127 qq! 128[realms] 129$realm = { 130 database_name = $kdc_datadir/principal 131 admin_keytab = FILE:$kdc_datadir/kadm5.keytab 132 acl_file = $kdc_datadir/kadm5.acl 133 key_stash_file = $kdc_datadir/_k5.$realm 134}!); 135 136mkdir $kdc_datadir or die; 137 138# Ensure that we use test's config and cache files, not global ones. 139$ENV{'KRB5_CONFIG'} = $krb5_conf; 140$ENV{'KRB5_KDC_PROFILE'} = $kdc_conf; 141$ENV{'KRB5CCNAME'} = $krb5_cache; 142 143my $service_principal = "$ENV{with_krb_srvnam}/$host"; 144 145system_or_bail $kdb5_util, 'create', '-s', '-P', 'secret0'; 146 147my $test1_password = 'secret1'; 148system_or_bail $kadmin_local, '-q', "addprinc -pw $test1_password test1"; 149 150system_or_bail $kadmin_local, '-q', "addprinc -randkey $service_principal"; 151system_or_bail $kadmin_local, '-q', "ktadd -k $keytab $service_principal"; 152 153system_or_bail $krb5kdc, '-P', $kdc_pidfile; 154 155END 156{ 157 kill 'INT', `cat $kdc_pidfile` if -f $kdc_pidfile; 158} 159 160note "setting up PostgreSQL instance"; 161 162my $node = get_new_node('node'); 163$node->init; 164$node->append_conf('postgresql.conf', "listen_addresses = '$hostaddr'"); 165$node->append_conf('postgresql.conf', "krb_server_keyfile = '$keytab'"); 166$node->start; 167 168$node->safe_psql('postgres', 'CREATE USER test1;'); 169 170note "running tests"; 171 172# Test connection success or failure, and if success, that query returns true. 173sub test_access 174{ 175 my ($node, $role, $query, $expected_res, $gssencmode, $test_name) = @_; 176 177 # need to connect over TCP/IP for Kerberos 178 my ($res, $stdoutres, $stderrres) = $node->psql( 179 'postgres', 180 undef, 181 extra_params => [ 182 '-XAtd', 183 $node->connstr('postgres') 184 . " host=$host hostaddr=$hostaddr $gssencmode", 185 '-U', 186 $role, 187 '-c', 188 $query 189 ]); 190 191 # If we get a query result back, it should be true. 192 if ($res == $expected_res and $res eq 0) 193 { 194 is($stdoutres, "t", $test_name); 195 } 196 else 197 { 198 is($res, $expected_res, $test_name); 199 } 200 return; 201} 202 203# As above, but test for an arbitrary query result. 204sub test_query 205{ 206 local $Test::Builder::Level = $Test::Builder::Level + 1; 207 208 my ($node, $role, $query, $expected, $gssencmode, $test_name) = @_; 209 210 # need to connect over TCP/IP for Kerberos 211 my ($res, $stdoutres, $stderrres) = $node->psql( 212 'postgres', 213 "$query", 214 extra_params => [ 215 '-XAtd', 216 $node->connstr('postgres') 217 . " host=$host hostaddr=$hostaddr $gssencmode", 218 '-U', 219 $role 220 ]); 221 222 is($res, 0, $test_name); 223 like($stdoutres, $expected, $test_name); 224 is($stderrres, "", $test_name); 225 return; 226} 227 228unlink($node->data_dir . '/pg_hba.conf'); 229$node->append_conf('pg_hba.conf', 230 qq{host all all $hostaddr/32 gss map=mymap}); 231$node->restart; 232 233test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket'); 234 235run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?); 236 237test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping'); 238 239$node->append_conf('pg_ident.conf', qq{mymap /^(.*)\@$realm\$ \\1}); 240$node->restart; 241 242test_access( 243 $node, 244 'test1', 245 'SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 246 0, 247 '', 248 'succeeds with mapping with default gssencmode and host hba'); 249test_access( 250 $node, 251 "test1", 252 'SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 253 0, 254 "gssencmode=prefer", 255 "succeeds with GSS-encrypted access preferred with host hba"); 256test_access( 257 $node, 258 "test1", 259 'SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 260 0, 261 "gssencmode=require", 262 "succeeds with GSS-encrypted access required with host hba"); 263 264# Test that we can transport a reasonable amount of data. 265test_query( 266 $node, 267 "test1", 268 'SELECT * FROM generate_series(1, 100000);', 269 qr/^1\n.*\n1024\n.*\n9999\n.*\n100000$/s, 270 "gssencmode=require", 271 "receiving 100K lines works"); 272 273test_query( 274 $node, 275 "test1", 276 "CREATE TABLE mytab (f1 int primary key);\n" 277 . "COPY mytab FROM STDIN;\n" 278 . join("\n", (1 .. 100000)) 279 . "\n\\.\n" 280 . "SELECT COUNT(*) FROM mytab;", 281 qr/^100000$/s, 282 "gssencmode=require", 283 "sending 100K lines works"); 284 285unlink($node->data_dir . '/pg_hba.conf'); 286$node->append_conf('pg_hba.conf', 287 qq{hostgssenc all all $hostaddr/32 gss map=mymap}); 288$node->restart; 289 290test_access( 291 $node, 292 "test1", 293 'SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 294 0, 295 "gssencmode=prefer", 296 "succeeds with GSS-encrypted access preferred and hostgssenc hba"); 297test_access( 298 $node, 299 "test1", 300 'SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 301 0, 302 "gssencmode=require", 303 "succeeds with GSS-encrypted access required and hostgssenc hba"); 304test_access($node, "test1", 'SELECT true', 2, "gssencmode=disable", 305 "fails with GSS encryption disabled and hostgssenc hba"); 306 307unlink($node->data_dir . '/pg_hba.conf'); 308$node->append_conf('pg_hba.conf', 309 qq{hostnogssenc all all $hostaddr/32 gss map=mymap}); 310$node->restart; 311 312test_access( 313 $node, 314 "test1", 315 'SELECT gss_authenticated and not encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 316 0, 317 "gssencmode=prefer", 318 "succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption" 319); 320test_access($node, "test1", 'SELECT true', 2, "gssencmode=require", 321 "fails with GSS-encrypted access required and hostnogssenc hba"); 322test_access( 323 $node, 324 "test1", 325 'SELECT gss_authenticated and not encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 326 0, 327 "gssencmode=disable", 328 "succeeds with GSS encryption disabled and hostnogssenc hba"); 329 330truncate($node->data_dir . '/pg_ident.conf', 0); 331unlink($node->data_dir . '/pg_hba.conf'); 332$node->append_conf('pg_hba.conf', 333 qq{host all all $hostaddr/32 gss include_realm=0}); 334$node->restart; 335 336test_access( 337 $node, 338 'test1', 339 'SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid();', 340 0, 341 '', 342 'succeeds with include_realm=0 and defaults'); 343