From 4c06bcd74a36ac83c2dcc6e3d0aadada09f42a70 Mon Sep 17 00:00:00 2001 From: Arnaud Durand Date: Thu, 24 Sep 2026 11:39:49 +0200 Subject: [PATCH 1/2] Set postgresql ALPN on SSL contexts created by asyncpg PostgreSQL 17+ rejects direct SSL connections without the `postgresql` ALPN protocol, so `direct_tls=True` only worked through TLS-terminating proxies. Set it on the contexts asyncpg creates, like libpq does, and document that user-provided contexts need it for direct TLS. Fixes #1371. --- asyncpg/connect_utils.py | 6 ++++++ asyncpg/connection.py | 6 ++++++ tests/test_connect.py | 14 ++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 61eae0c0..b9076c25 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -35,6 +35,9 @@ _SSL_REQUEST_CODE = 80877103 +# Required by PostgreSQL 17+ for direct TLS connections. +_ALPN_PROTOCOLS = ['postgresql'] + class SSLMode(enum.IntEnum): disable = 0 @@ -811,8 +814,11 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl_max_protocol_version ) + ssl.set_alpn_protocols(_ALPN_PROTOCOLS) + elif ssl is True: ssl = ssl_module.create_default_context() + ssl.set_alpn_protocols(_ALPN_PROTOCOLS) sslmode = SSLMode.verify_full elif isinstance(ssl, ssl_module.SSLContext): sslmode = SSLMode.require diff --git a/asyncpg/connection.py b/asyncpg/connection.py index d28d95c4..e3186bc3 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -2337,6 +2337,10 @@ async def connect(dsn=None, *, Pass ``True`` to skip PostgreSQL STARTTLS mode and perform a direct SSL connection. Requires ``ssl='require'``, ``'verify-ca'``, ``'verify-full'``, ``True``, or an explicit ``SSLContext``. + PostgreSQL 17+ requires the ``postgresql`` ALPN protocol for direct + SSL connections: asyncpg sets it on the contexts it creates, but an + explicit ``SSLContext`` must set it with + ``ctx.set_alpn_protocols(['postgresql'])``. :param dict server_settings: An optional dict of server runtime parameters. Refer to @@ -2456,6 +2460,8 @@ async def connect(dsn=None, *, ``ssl=True``, or an explicit ``SSLContext``. Other values (``'disable'``, ``'allow'``, and ``'prefer'``) will raise a ``ClientConfigurationError``. + SSL contexts created by asyncpg now set the ``postgresql`` ALPN + protocol, which PostgreSQL 17+ requires for direct SSL connections. .. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext .. _create_default_context: diff --git a/tests/test_connect.py b/tests/test_connect.py index e00c7b6c..66341f40 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -2331,6 +2331,20 @@ async def test_direct_tls_connection(self): self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) self.assertFalse(ctx.check_hostname) + async def test_direct_tls_native(self): + if self.cluster.get_pg_version() < (17, 0): + self.skipTest('native direct TLS requires PostgreSQL 17+') + + for mode in ('require', 'verify-ca', 'verify-full', True): + with self.subTest(mode=mode): + with unittest.mock.patch.dict(os.environ, { + 'SSL_CERT_FILE': SSL_CA_CERT_FILE, + }): + await self._test_works( + dsn='postgresql://ssl_user@localhost/postgres' + '?sslrootcert=' + SSL_CA_CERT_FILE, + ssl=mode, direct_tls=True, expected_ssl=True) + async def test_direct_tls_configuration_sources(self): base_dsn = 'postgresql://ssl_user@localhost/postgres' From fa80694e4449af12c00bb3de06e1841c515b4c96 Mon Sep 17 00:00:00 2001 From: Arnaud Durand Date: Fri, 25 Sep 2026 10:36:39 +0200 Subject: [PATCH 2/2] Skip ALPN setup when the ssl module lacks ALPN support SSLContext.set_alpn_protocols() raises NotImplementedError on builds without ALPN, which broke every default (sslmode=prefer) TCP connection. --- asyncpg/connect_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index b9076c25..528739d0 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -814,11 +814,13 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl_max_protocol_version ) - ssl.set_alpn_protocols(_ALPN_PROTOCOLS) + if ssl_module.HAS_ALPN: + ssl.set_alpn_protocols(_ALPN_PROTOCOLS) elif ssl is True: ssl = ssl_module.create_default_context() - ssl.set_alpn_protocols(_ALPN_PROTOCOLS) + if ssl_module.HAS_ALPN: + ssl.set_alpn_protocols(_ALPN_PROTOCOLS) sslmode = SSLMode.verify_full elif isinstance(ssl, ssl_module.SSLContext): sslmode = SSLMode.require