libpq: Add oauth_ca_file option to change CAs without debugging

PG18 hid the PGOAUTHCAFILE envvar behind PGOAUTHDEBUG=UNSAFE, because I
thought that any "real" production usage of private CA certificates
would have them added to the Curl system trust store. But there are use
cases, such as containerized environments, that prefer to manage custom
CA settings more granularly; some of them consider envvar configuration
of certificates to be standard practice.

Move PGOAUTHCAFILE out from under the debug flag, and add an
oauth_ca_file option to libpq to configure trusted CAs per connection.

Patch by Jonathan Gonzalez V., with some additional wordsmithing and
test organization by me.

Author: Jonathan Gonzalez V. <jonathan.abdiel@gmail.com>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com>
Discussion: https://postgr.es/m/16a91d02795cb991963326a902afa764e4d721db.camel%40gmail.com
This commit is contained in:
Jacob Champion 2026-03-30 14:14:45 -07:00
parent bab2f27eaa
commit 993368113c
6 changed files with 95 additions and 44 deletions

View file

@ -2585,6 +2585,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
<varlistentry id="libpq-connect-oauth-ca-file" xreflabel="oauth_ca_file">
<term><literal>oauth_ca_file</literal></term>
<listitem>
<para>
The name of a file containing one or more SSL certificate authority
(<acronym>CA</acronym>) certificates, which will be used to verify the
identity of the authorization server and its endpoints. By default, the
<productname>Curl</productname> system certificate bundle is used.
</para>
<para>
This parameter does not affect verification of the
<productname>PostgreSQL</productname> server certificate; see
<xref linkend="libpq-connect-sslrootcert"/> instead.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</sect2>
@ -9422,6 +9439,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
linkend="libpq-connect-max-protocol-version"/> connection parameter.
</para>
</listitem>
<listitem>
<para>
<indexterm>
<primary><envar>PGOAUTHCAFILE</envar></primary>
</indexterm>
<envar>PGOAUTHCAFILE</envar> behaves the same as the <xref
linkend="libpq-connect-oauth-ca-file"/> connection parameter.
</para>
</listitem>
</itemizedlist>
</para>
@ -10608,6 +10635,13 @@ typedef struct
<sect2 id="libpq-oauth-debugging">
<title>Debugging and Developer Settings</title>
<para>
While developing against a local authorization server, it may be helpful to
make use of the <xref linkend="libpq-connect-oauth-ca-file"/> connection
parameter (or the equivalent <envar>PGOAUTHCAFILE</envar> environment
variable) in the client application.
</para>
<para>
A "dangerous debugging mode" may be enabled by setting the environment
variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
@ -10620,12 +10654,6 @@ typedef struct
permits the use of unencrypted HTTP during the OAuth provider exchange
</para>
</listitem>
<listitem>
<para>
allows the system's trusted CA list to be completely replaced using the
<envar>PGOAUTHCAFILE</envar> environment variable
</para>
</listitem>
<listitem>
<para>
prints HTTP traffic (containing several critical secrets) to standard

View file

@ -216,6 +216,7 @@ struct async_ctx
/* relevant connection options cached from the PGconn */
char *client_id; /* oauth_client_id */
char *client_secret; /* oauth_client_secret (may be NULL) */
char *ca_file; /* oauth_ca_file */
/* options cached from the PGoauthBearerRequest (we don't own these) */
const char *discovery_uri;
@ -336,6 +337,7 @@ free_async_ctx(struct async_ctx *actx)
free(actx->client_id);
free(actx->client_secret);
free(actx->ca_file);
free(actx);
}
@ -1833,21 +1835,9 @@ setup_curl_handles(struct async_ctx *actx)
CHECK_SETOPT(actx, popt, protos, return false);
}
/*
* If we're in debug mode, allow the developer to change the trusted CA
* list. For now, this is not something we expose outside of the UNSAFE
* mode, because it's not clear that it's useful in production: both libpq
* and the user's browser must trust the same authorization servers for
* the flow to work at all, so any changes to the roots are likely to be
* done system-wide.
*/
if (actx->debugging)
{
const char *env;
if ((env = getenv("PGOAUTHCAFILE")) != NULL)
CHECK_SETOPT(actx, CURLOPT_CAINFO, env, return false);
}
/* Allow the user to change the trusted CA list. */
if (actx->ca_file != NULL)
CHECK_SETOPT(actx, CURLOPT_CAINFO, actx->ca_file, return false);
/*
* Suppress the Accept header to make our request as minimal as possible.
@ -3125,6 +3115,12 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
if (!actx->client_secret)
goto oom;
}
else if (strcmp(opt->keyword, "oauth_ca_file") == 0)
{
actx->ca_file = strdup(opt->val);
if (!actx->ca_file)
goto oom;
}
}
PQconninfoFree(conninfo);

View file

@ -413,6 +413,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"OAuth-Scope", "", 15,
offsetof(struct pg_conn, oauth_scope)},
{"oauth_ca_file", "PGOAUTHCAFILE", NULL, NULL,
"OAuth-CA-File", "", 64,
offsetof(struct pg_conn, oauth_ca_file)},
{"sslkeylogfile", NULL, NULL, NULL,
"SSL-Key-Log-File", "D", 64,
offsetof(struct pg_conn, sslkeylogfile)},
@ -5158,6 +5162,7 @@ freePGconn(PGconn *conn)
free(conn->oauth_discovery_uri);
free(conn->oauth_client_id);
free(conn->oauth_client_secret);
free(conn->oauth_ca_file);
free(conn->oauth_scope);
/* Note that conn->Pfdebug is not ours to close or free */
free(conn->events);

View file

@ -444,6 +444,7 @@ struct pg_conn
char *oauth_client_secret; /* client secret */
char *oauth_scope; /* access token scope */
char *oauth_token; /* access token */
char *oauth_ca_file; /* CA file path */
bool oauth_want_retry; /* should we retry on failure? */
/* Optional file to write trace info to */

View file

@ -71,9 +71,9 @@ END
$? = $exit_code;
}
# To test against HTTPS with our custom CA, we need to enable PGOAUTHDEBUG and
# PGOAUTHCAFILE. But first, check to make sure the client refuses HTTP and
# untrusted HTTPS connections by default.
# To test against HTTPS with our custom CA, we'll set PGOAUTHCAFILE. But first,
# check to make sure the client refuses HTTP and untrusted HTTPS connections by
# default.
my $port = $webserver->port();
my $issuer = "http://127.0.0.1:$port";
@ -119,28 +119,46 @@ is( $contents,
3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}},
"pg_hba_file_rules recreates OAuth HBA settings");
# Make sure PGOAUTHDEBUG=UNSAFE doesn't disable certificate verification.
$ENV{PGOAUTHDEBUG} = "UNSAFE";
{
# Make sure PGOAUTHDEBUG=UNSAFE doesn't disable certificate verification.
local $ENV{PGOAUTHDEBUG} = "UNSAFE";
$node->connect_fails(
"user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
"HTTPS trusts only system CA roots by default",
# Note that the latter half of this error message comes from Curl, which has
# had a few variants since 7.61:
#
# - SSL peer certificate or SSH remote key was not OK
# - Peer certificate cannot be authenticated with given CA certificates
# - Issuer check against peer certificate failed
#
# Key off of the "peer certificate" portion, since that seems to have
# remained constant over a long period of time.
expected_stderr =>
qr/failed to fetch OpenID discovery document:.*peer certificate/i);
# Now we can use our alternative CA.
$ENV{PGOAUTHCAFILE} = "$ENV{cert_dir}/root+server_ca.crt";
$node->connect_fails(
"user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
"HTTPS trusts only system CA roots by default",
# Note that the latter half of this error message comes from Curl, which
# has had a few variants since 7.61:
#
# - SSL peer certificate or SSH remote key was not OK
# - Peer certificate cannot be authenticated with given CA certificates
# - Issuer check against peer certificate failed
#
# Key off of the "peer certificate" portion, since that seems to have
# remained constant over a long period of time.
expected_stderr =>
qr/failed to fetch OpenID discovery document:.*peer certificate/i);
}
my $alternative_ca = "$ENV{cert_dir}/root+server_ca.crt";
my $user = "test";
# Make sure we can use oauth_ca_file option to specify the alternative CA path
$node->connect_ok(
"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635 oauth_ca_file=$alternative_ca",
"connect as test (oauth_ca_file)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@,
log_like => [
qr/oauth_validator: token="9243959234", role="$user"/,
qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
qr/connection authenticated: identity="test" method=oauth/,
qr/connection authorized/,
]);
# Make sure that we can use the environment variable without PGOAUTHDEBUG, and
# then use it for the rest of the tests
$ENV{PGOAUTHCAFILE} = $alternative_ca;
$node->connect_ok(
"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
"connect as test",
@ -153,6 +171,9 @@ $node->connect_ok(
qr/connection authorized/,
]);
# Enable PGOAUTHDEBUG for all remaining tests.
$ENV{PGOAUTHDEBUG} = "UNSAFE";
# The /alternate issuer uses slightly different parameters, along with an
# OAuth-style discovery document.
$user = "testalt";

View file

@ -28,7 +28,7 @@ daemon implemented in t/oauth_server.py. (Python has a fairly usable HTTP server
in its standard library, so the implementation was ported from Perl.)
This authorization server serves HTTPS on 127.0.0.1 (IPv4 only). libpq will need
to set PGOAUTHDEBUG=UNSAFE and PGOAUTHCAFILE with the right CA.
to set PGOAUTHCAFILE with the right CA.
=cut