From 0864fb5e0f478b693f84db8bc542f0e46e141d19 Mon Sep 17 00:00:00 2001 From: Ajit Awekar Date: Tue, 30 Jun 2026 16:52:32 +0530 Subject: [PATCH 4/5] Add GSS credential expiry to credential validation Register a method-specific validator for GSSAPI/Kerberos sessions (CVT_GSS). The new helper be_gssapi_get_context_expired() uses gss_context_time() on the acceptor context retained on the Port to report, with no round-trip to the KDC, whether the context lifetime derived from the client's Kerberos ticket has elapsed. A short-lived-ticket TAP test (009_gss_continuous_validation.pl) confirms an idle GSS session is terminated once the credential lapses; PostgreSQL::Test::Kerberos gains clockskew and ticket-lifetime parameters and namespaces its KDC files by test script name. --- src/backend/libpq/auth-validate-methods.c | 33 ++++ src/backend/libpq/auth-validate.c | 2 + src/backend/libpq/be-secure-gssapi.c | 37 +++++ src/include/libpq/auth-validate.h | 1 + src/include/libpq/libpq-be.h | 7 + src/test/kerberos/meson.build | 1 + .../t/009_gss_continuous_validation.pl | 144 ++++++++++++++++++ src/test/perl/PostgreSQL/Test/Kerberos.pm | 40 +++-- 8 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 src/test/kerberos/t/009_gss_continuous_validation.pl diff --git a/src/backend/libpq/auth-validate-methods.c b/src/backend/libpq/auth-validate-methods.c index 0b74057fffd..7039b3201b8 100644 --- a/src/backend/libpq/auth-validate-methods.c +++ b/src/backend/libpq/auth-validate-methods.c @@ -31,6 +31,7 @@ /* Function declarations for internal use */ static bool validate_oauth_credentials(void); static bool validate_cert_credentials(void); +static bool validate_gss_credentials(void); /* * Initialize validation methods @@ -46,6 +47,7 @@ InitializeValidationMethods(void) */ RegisterCredentialValidator(CVT_OAUTH, validate_oauth_credentials); RegisterCredentialValidator(CVT_CERT, validate_cert_credentials); + RegisterCredentialValidator(CVT_GSS, validate_gss_credentials); } /* @@ -134,3 +136,34 @@ validate_cert_credentials(void) return true; } + +/* + * Validate GSSAPI (Kerberos) credentials. + * + * The GSS security context established at authentication time carries a + * lifetime derived from the Kerberos ticket the client presented at connection + * time. Once that lifetime has elapsed, the credential that authenticated the + * session is no longer valid. This is checked locally from the context + * retained on the Port, with no round-trip to the KDC. Returns false once the + * context has expired, true otherwise. + * + * If the session is not using a GSS context (which should not happen for a + * GSS-authenticated session), there is nothing GSS-specific to validate, so + * the credentials are considered valid. + */ +static bool +validate_gss_credentials(void) +{ +#ifdef ENABLE_GSS + Port *port = MyProcPort; + + if (port == NULL) + return true; + + /* The session is no longer valid once the GSS context expires */ + if (be_gssapi_get_context_expired(port)) + return false; +#endif + + return true; +} diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c index fd31be1ca99..e39a33b71a9 100644 --- a/src/backend/libpq/auth-validate.c +++ b/src/backend/libpq/auth-validate.c @@ -48,6 +48,8 @@ UserAuthToValidationType(UserAuth auth_method) return CVT_OAUTH; case uaCert: return CVT_CERT; + case uaGSS: + return CVT_GSS; default: /* * No method-specific validator for other auth methods. Password diff --git a/src/backend/libpq/be-secure-gssapi.c b/src/backend/libpq/be-secure-gssapi.c index 540ed62a5cc..2e6827c2c9f 100644 --- a/src/backend/libpq/be-secure-gssapi.c +++ b/src/backend/libpq/be-secure-gssapi.c @@ -788,3 +788,40 @@ be_gssapi_get_delegation(Port *port) return port->gss->delegated_creds; } + +/* + * Report whether the GSSAPI security context established at authentication + * time has passed its expiration. + * + * The lifetime of the GSS context is derived from the Kerberos ticket the + * client presented at connection time. gss_context_time() reports how many + * seconds the context will remain valid; once that reaches zero (or the call + * reports GSS_S_CONTEXT_EXPIRED) the credential that authenticated this + * session has expired. This is a purely local check and involves no + * round-trip to the KDC. + * + * Returns true if a context is present and has expired, false otherwise. If + * no GSS context is present, or the lifetime cannot be read, the session is + * conservatively treated as not expired so that callers do not terminate it on + * the basis of missing or unreadable state. + */ +bool +be_gssapi_get_context_expired(Port *port) +{ + OM_uint32 major, + minor, + lifetime; + + if (!port || !port->gss || port->gss->ctx == GSS_C_NO_CONTEXT) + return false; + + major = gss_context_time(&minor, port->gss->ctx, &lifetime); + + if (major == GSS_S_CONTEXT_EXPIRED) + return true; + + if (GSS_ERROR(major)) + return false; + + return (lifetime == 0); +} diff --git a/src/include/libpq/auth-validate.h b/src/include/libpq/auth-validate.h index b0b5a1144a7..5a0e1df4dcf 100644 --- a/src/include/libpq/auth-validate.h +++ b/src/include/libpq/auth-validate.h @@ -21,6 +21,7 @@ typedef enum CredentialValidationType { CVT_OAUTH = 0, /* OAuth bearer token authentication */ CVT_CERT, /* TLS client certificate authentication */ + CVT_GSS, /* GSSAPI/Kerberos authentication */ CVT_COUNT /* Total number of credential validation types */ } CredentialValidationType; diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index aa007da958c..f3fae64623d 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -353,6 +353,13 @@ extern bool be_gssapi_get_enc(Port *port); extern const char *be_gssapi_get_princ(Port *port); extern bool be_gssapi_get_delegation(Port *port); +/* + * Report whether the GSSAPI security context established at authentication + * time has passed its expiration (derived from the client's Kerberos ticket + * lifetime). Returns false when no GSS context is present. + */ +extern bool be_gssapi_get_context_expired(Port *port); + /* Read and write to a GSSAPI-encrypted connection. */ extern ssize_t be_gssapi_read(Port *port, void *ptr, size_t len); extern ssize_t be_gssapi_write(Port *port, const void *ptr, size_t len); diff --git a/src/test/kerberos/meson.build b/src/test/kerberos/meson.build index 11aa732e69b..7d063956205 100644 --- a/src/test/kerberos/meson.build +++ b/src/test/kerberos/meson.build @@ -8,6 +8,7 @@ tests += { 'test_kwargs': {'priority': 40}, # kerberos tests are slow, start early 'tests': [ 't/001_auth.pl', + 't/009_gss_continuous_validation.pl', ], 'env': { 'with_gssapi': gssapi.found() ? 'yes' : 'no', diff --git a/src/test/kerberos/t/009_gss_continuous_validation.pl b/src/test/kerberos/t/009_gss_continuous_validation.pl new file mode 100644 index 00000000000..e683a59900b --- /dev/null +++ b/src/test/kerberos/t/009_gss_continuous_validation.pl @@ -0,0 +1,144 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +# Test continuous credential validation for GSSAPI/Kerberos sessions. +# +# A session authenticated with GSSAPI keeps running independently of the +# Kerberos ticket that established it; nothing re-checks the ticket once the +# connection is up. With credential_validation_enabled, the backend should +# periodically re-verify that the GSS security context (whose lifetime is +# derived from the client's ticket) has not expired, and terminate the session +# once it has. +# +# This test sets up a KDC, obtains a short-lived ticket, opens a GSS session, +# lets the ticket (and therefore the server-side GSS context) expire while the +# session sits idle, and then verifies that the next command is rejected with a +# FATAL "session credentials have expired". +# +# Like 001_auth.pl this requires a full MIT Kerberos installation; see the +# README for details. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Kerberos; +use Test::More; + +if ($ENV{with_gssapi} ne 'yes') +{ + plan skip_all => 'GSSAPI/Kerberos not supported by this build'; +} +elsif (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bkerberos\b/) +{ + plan skip_all => + 'Potentially unsafe test GSSAPI/Kerberos not enabled in PG_TEST_EXTRA'; +} + +# Timing parameters. The server-side GSS (acceptor) context lifetime is not +# the client's Kerberos ticket lifetime; it is bounded by the allowable clock +# skew. We therefore configure a small clock skew so that the GSS context +# expires shortly after authentication, and then sit idle long enough that the +# context is certainly expired ($idle_seconds > $clockskew) and the validation +# timer has certainly fired ($idle_seconds > $validation_interval, whose +# minimum is 5s) before issuing the next command. +my $validation_interval = 5; +my $clockskew = 5; +my $ticket_lifetime = '15s'; +my $idle_seconds = 25; + +my $dbname = 'postgres'; +my $username = 'test1'; + +note "setting up Kerberos"; + +my $host = 'auth-test-localhost.postgresql.example.com'; +my $hostaddr = '127.0.0.1'; +my $realm = 'EXAMPLE.COM'; + +my $krb = + PostgreSQL::Test::Kerberos->new($host, $hostaddr, $realm, + clockskew => $clockskew); + +my $test1_password = 'secret1'; +$krb->create_principal('test1', $test1_password); + +note "setting up PostgreSQL instance"; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq{ +listen_addresses = '$hostaddr' +krb_server_keyfile = '$krb->{keytab}' +log_connections = all +log_min_messages = debug2 +lc_messages = 'C' +credential_validation_enabled = on +credential_validation_interval = $validation_interval +}); +$node->start; + +$node->safe_psql('postgres', 'CREATE USER test1;'); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', + qq{ +host all all $hostaddr/32 gss include_realm=0 +}); +$node->restart; + +note "running tests"; + +# The server-side GSS (acceptor) context lifetime is bounded by the larger of +# the ticket's remaining lifetime and the allowable clock skew, so force both +# to be small: a short ticket together with the small clock skew configured +# above makes the GSS context expire shortly after the session is established. +$krb->create_ticket('test1', $test1_password, lifetime => $ticket_lifetime); + +# Open a GSS-authenticated session, confirm it is live, then sit idle past the +# GSS context lifetime (and the validation interval) before issuing another +# command. +# The idle period is spent in a client-side "\! sleep", so the backend waits at +# a command boundary -- exactly where the validation timer's pending flag is +# acted upon when the next command arrives. +# +# Use gssencmode=disable so that GSSAPI is used only for authentication and the +# transport stays in plaintext. With GSS transport encryption the expiring +# context would break the connection at the transport layer (and trigger a +# libpq reconnect) before the backend's credential validation runs; disabling +# it isolates the behavior under test -- the server terminating the session +# because its credentials expired. +my $connstr = $node->connstr('postgres') + . " user=$username host=$host hostaddr=$hostaddr gssencmode=disable"; + +my $script = + "SELECT 'session-live';\n" . "\\! sleep $idle_seconds\n" . "SELECT 'after-expiry';\n"; + +my ($ret, $stdout, $stderr) = $node->psql( + 'postgres', $script, + connstr => $connstr, + extra_params => ['-w']); + +isnt($ret, 0, 'GSS session is terminated once its credentials expire'); +like( + $stdout, + qr/session-live/, + 'session is live before the GSS credentials expire'); +unlike( + $stdout, + qr/after-expiry/, + 'command after expiry does not return a result'); +like( + $stderr, + qr/session credentials have expired/, + 'session terminated with the credential-expiry FATAL'); + +# The session must have actually authenticated via GSS for this to be a +# meaningful test of GSS credential validation. +ok( $node->log_contains( + qq{connection authenticated: identity="test1\@$realm" method=gss}), + 'session was authenticated with GSS'); + +done_testing(); diff --git a/src/test/perl/PostgreSQL/Test/Kerberos.pm b/src/test/perl/PostgreSQL/Test/Kerberos.pm index e861d93533e..39de8f918b8 100644 --- a/src/test/perl/PostgreSQL/Test/Kerberos.pm +++ b/src/test/perl/PostgreSQL/Test/Kerberos.pm @@ -8,6 +8,7 @@ package PostgreSQL::Test::Kerberos; use strict; use warnings FATAL => 'all'; +use File::Basename; use PostgreSQL::Test::Utils; use Test::More; @@ -62,15 +63,22 @@ INIT $krb5kdc = $krb5_sbin_dir . '/' . $krb5kdc; } - $krb5_conf = "${PostgreSQL::Test::Utils::tmp_check}/krb5.conf"; - $kdc_conf = "${PostgreSQL::Test::Utils::tmp_check}/kdc.conf"; - $krb5_cache = "${PostgreSQL::Test::Utils::tmp_check}/krb5cc"; - $krb5_log = "${PostgreSQL::Test::Utils::log_path}/krb5libs.log"; - $kdc_log = "${PostgreSQL::Test::Utils::log_path}/krb5kdc.log"; + # Derive a per-test prefix from the running script's name so that the KDC + # files, which live in the shared tmp_check/log directories, do not collide + # when more than one Kerberos test script is run in the same build (each + # script sets up its own KDC). This mirrors how Cluster.pm namespaces node + # data directories. + my $prefix = basename($0); + + $krb5_conf = "${PostgreSQL::Test::Utils::tmp_check}/${prefix}_krb5.conf"; + $kdc_conf = "${PostgreSQL::Test::Utils::tmp_check}/${prefix}_kdc.conf"; + $krb5_cache = "${PostgreSQL::Test::Utils::tmp_check}/${prefix}_krb5cc"; + $krb5_log = "${PostgreSQL::Test::Utils::log_path}/${prefix}_krb5libs.log"; + $kdc_log = "${PostgreSQL::Test::Utils::log_path}/${prefix}_krb5kdc.log"; $kdc_port = PostgreSQL::Test::Cluster::get_free_port(); - $kdc_datadir = "${PostgreSQL::Test::Utils::tmp_check}/krb5kdc"; - $kdc_pidfile = "${PostgreSQL::Test::Utils::tmp_check}/krb5kdc.pid"; - $keytab = "${PostgreSQL::Test::Utils::tmp_check}/krb5.keytab"; + $kdc_datadir = "${PostgreSQL::Test::Utils::tmp_check}/${prefix}_krb5kdc"; + $kdc_pidfile = "${PostgreSQL::Test::Utils::tmp_check}/${prefix}_krb5kdc.pid"; + $keytab = "${PostgreSQL::Test::Utils::tmp_check}/${prefix}_krb5.keytab"; } =pod @@ -102,7 +110,13 @@ Name of the Kerberos realm. sub new { my $class = shift; - my ($host, $hostaddr, $realm) = @_; + my ($host, $hostaddr, $realm, %params) = @_; + + # Optionally override the allowable clock skew. The acceptor's GSS context + # lifetime is bounded by this value, so tests that need a GSS context to + # expire quickly can set a small clock skew. + my $clockskew_line = + defined $params{clockskew} ? "clockskew = $params{clockskew}\n" : ""; my ($stdout, $krb5_version); run_log [ $krb5_config, '--version' ], '>' => \$stdout @@ -140,7 +154,8 @@ dns_lookup_kdc = false default_realm = $realm forwardable = false rdns = false - +dns_canonicalize_hostname = false +$clockskew_line [realms] $realm = { kdc = $hostaddr:$kdc_port @@ -222,6 +237,11 @@ sub create_ticket push @cmd, '-f' if ($params{forwardable}); + # Request a specific ticket lifetime (e.g. '10s') when asked. This is + # used to test behavior that depends on credentials expiring, such as + # continuous credential validation. + push @cmd, ('-l', $params{lifetime}) if (defined $params{lifetime}); + run_log [@cmd], \$password or BAIL_OUT($?); run_log [ $klist, '-f' ] or BAIL_OUT($?); } -- 2.52.0