Re: Proposal: Supporting URI SAN in Certificate Authentication

From: Agustín Martínez Fayó <amartinezfayo(at)gmail(dot)com>
To: Florin Irion <irionr(at)gmail(dot)com>
Cc: Jacob Champion <jacob(dot)champion(at)enterprisedb(dot)com>, olivier cano <kindermoumoute(at)gmail(dot)com>, pgsql-hackers(at)lists(dot)postgresql(dot)org, david(at)pgbackrest(dot)org, gabriele(dot)bartolini(at)enterprisedb(dot)com
Subject: Re: Proposal: Supporting URI SAN in Certificate Authentication
Date: 2026-09-25 23:19:43
Message-ID: CAJcBgCeqn0QZ6ZSO9=9SfHn48x=tK47g8n_OfCe7aNPmKzo=zQ@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On 22/09/2026 11:46, Florin Irion wrote:
> The single-URI requirement is a design simplification, not a spec requirement.
> SPIFFE motivated the feature but doesn't necessarily govern it. If someone
> later proposes matching against multiple URI SANs, it can be evaluated on its
> own merits.

Hi all,

Thank you Florin for the introduction. The SPIFFE community has wanted
native URI SAN authentication in PostgreSQL for a long time, since
integrations so far have typically had to go through a DNS SAN copied
into the CN, so I am glad to see this happening. I wanted to share a few
thoughts on the discussion so far, and some comments on v2.

I agree with treating the single URI as a design simplification rather
than something X509-SVID imposes. It keeps the existing model of one
identity string per certificate, the same as CN and DN, and it only
applies on lines that opt into clientname=URI, so I would not expect it
to break anyone. I also think strict first is the ordering that stays
compatible, since any-of-N matching could be added later as an opt-in
without affecting anyone who relied on the single-URI rule, whereas the
reverse would be a behavior change. For what it is worth, X509-SVID
requires a single URI SAN for the auditing and authorization reasons
Florin mentioned [1], so SPIFFE deployments would not be affected by a
later multi-URI option either way, since their certificates carry one
URI and would behave the same under both rules.

I also agree with not adding the other X509-SVID validation checks. As I
see it, PostgreSQL is a relying party consuming a client certificate
rather than an SVID validator. The leaf checks in section 5.2 [2] (cA
false, no keyCertSign or cRLSign, spiffe scheme, non-root path) are only
defeatable by someone holding a CA key, who could mint any identity
anyway, and the scheme and path are pinned in practice by the pg_ident
entry (for a literal entry or an anchored regex). The one SPIFFE
property I do not think PostgreSQL can provide today is binding the
trust domain in the ID to the CA that issued it, since ssl_ca_file is a
flat list. The Federation spec says bundles from different trust domains
must not be merged for that reason [3], so the caveat in v2 (one trust
domain per ssl_ca_file, federation unsupported) matches what the spec
expects.

Longer term, I think the SPIFFE community would welcome a SPIFFE-aware
trust mode in PostgreSQL, where each configured trust domain has its own
bundle and the trust domain in the ID selects which bundle validates the
chain, similar to Envoy's SPIFFE certificate validator [4]. That would
be a separate design with its own configuration questions, so I am not
proposing it for this patch. I only mention it because the URI SAN match
is the substrate such a mode would build on, and v2 already keeps that
door open by recording the URI as the identity. What I think would be
great to have in v1 is a short worked example in the documentation, a
pg_hba line with clientname=URI and a pg_ident entry mapping a SPIFFE ID
to a role, so that people looking for SPIFFE support can find the
feature.

I built v2, ran the SSL TAP suite, and tried a couple of things beyond
it. A few comments on the patch follow.

> +$node->connect_fails(
> + "$uri_connstr user=ssltestuser sslcert=ssl/client-uri-multi.crt "
> + . sslkey('client-uri-multi.key'),
> + "certificate authorization fails when client certificate has multiple URI SANs",
> + expected_stderr =>
> + qr/certificate authentication failed for user "ssltestuser"/);

I think this does not exercise the count check. client-uri-multi.config
lists the non-matching URI first and the code keeps the first URI, so
without the check the connection is authenticated as
spiffe://.../not-the-user and then fails on the usermap, which produces
the same client error. I deleted the check and the suite still passes
286/286. I wonder if putting the matching URI first in the fixture, or
asserting the server log reason with log_like, would make the test fail
for the right cause.

> + /* fail closed if the URI could not be converted */
> + if (port->peer_uri_count > 0 && port->peer_uri == NULL)
> + port->peer_uri_count = 0;

This seems to log a COMMERROR while the handshake still completes. I
signed a certificate with the suite's client CA whose single URI SAN
contains a null byte. On a clientname=CN line the log shows "URI subject
alternative name contains embedded null" and the connection is then
authenticated, with SELECT system_user returning cert:CN=ssltestuser. On
a clientname=URI line the same log line is followed by the single-URI
rejection message, which points at the wrong cause. Returning false
here, as the CN path does for an embedded null, would also reject such
certificates on CN and DN lines, which never looked at URI SANs before
this patch. I think that is a reasonable price for consistency, but the
alternative of failing only on URI lines with an accurate error message
would work too.

> + if (X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253) == -1 ||
> + BIO_get_mem_ptr(bio, &bio_buf) < 0)

I wonder whether relaxing this from <= 0 was needed. With OpenSSL 3.x
and 1.1.1, X509_NAME_print_ex returns 0 for an empty name and
BIO_get_mem_ptr still returns 1. If BIO_get_mem_ptr ever returned 0,
bio_buf would stay NULL for the dereference below, so the original check
seems safer to me.

> + contain exactly one URI subject alternative name. The comparison is
> + case-sensitive and uses the exact URI as it appears in the
> + certificate.

Minor. Since the URI is matched as an opaque string, I wonder if a
sentence advising that regular-expression maps anchor the full scheme
and authority would help avoid patterns that match a URI from an
unexpected authority.

> + Note that PostgreSQL does not verify any relationship between a
> + certificate's URI and the certificate authority that signed the
> + certificate. If <literal>ssl_ca_file</literal> contains certificates
> + for more than one trust domain, any of them can issue a certificate
> + for a URI in another trust domain. For SPIFFE deployments, configure

Minor. This uses "trust domain" before SPIFFE is introduced. Maybe it
could say that any CA in the file can issue a certificate carrying any
URI, and keep the trust domain wording for the SPIFFE sentence that
follows.

Also minor, and outside the hunks. The cert method section ("The cn
(Common Name) attribute of the certificate will be compared") and the
clientcert=verify-full description ("enforces that the cn (Common Name)
in the certificate matches") still describe CN only, and the client
certificates section in runtime.sgml says the same thing in three
places. With three clientname values now, I think a pointer to
clientname from those places would help.

Everything else looks right to me from the SPIFFE side. The matched URI
is the recorded identity, an empty Subject with a critical URI SAN is
accepted and tested (the shape RFC 5280 prescribes when the only name is
in the SAN [5]), and the byte-for-byte comparison is documented. The
SPIFFE ID spec treats the scheme and trust domain as case-insensitive
when parsing [6], but also requires the trust domain to be
lowercase [7], and go-spiffe rejects a trust domain that is not
lowercase rather than normalizing it [8]. So I think treating the URI as
an opaque, case-sensitive string is consistent with the reference
implementation, and a certificate that differs only in case fails to
match rather than matching unexpectedly.

Happy to keep testing against SPIRE-issued SVIDs as the patch evolves.

Regards,
Agustín

[1] https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#2-spiffe-id
[2] https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#52-leaf-validation
[3] https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#42-managing-fetched-bundles
[4] https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto
[5] https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.6
[6] https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#24-spiffe-id-parsing
[7] https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#21-trust-domain
[8] https://github.com/spiffe/go-spiffe/blob/main/spiffeid/trustdomain.go

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Bharath Rupireddy 2026-09-25 23:25:18 Re: WAL segment file descriptor leak on read errors can PANIC the server
Previous Message shihao zhong 2026-09-25 22:49:07 Re: REPACK (CONCURRENTLY) can't complete after ~105M concurrent updates/deletes