Skip to content

Commit e92678e

Browse files
committed
connection: fix ssl_options TLS handling
Preserve the distinction between omitted and explicitly empty ssl_options across connection setup, reactors, shard-aware routing, cloud contexts, and Insights reporting. Strengthen pyOpenSSL protocol, verification, SNI, and hostname handling while retaining legacy option behavior.
1 parent b8b714c commit e92678e

25 files changed

Lines changed: 3074 additions & 207 deletions

CHANGELOG.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ Features
77
statements skip re-sending result metadata on EXECUTE, and the driver automatically
88
refreshes cached metadata when the server detects a schema change (DRIVER-153)
99

10+
Bug Fixes
11+
---------
12+
* Explicit ``ssl_options={}`` and endpoint SSL options now enable TLS instead
13+
of being treated as omitted SSL options. Empty options without additional
14+
endpoint security settings encrypt the connection without verifying the
15+
server certificate. Cluster-level ``ssl_options=None`` does not enable TLS by
16+
itself; an ``ssl_context`` or endpoint SSL options still enable it. An
17+
endpoint ``server_hostname`` used only for SNI routing is not treated as an
18+
additional security setting for explicit empty cluster options.
19+
* Legacy ``ssl_options`` now work with the Twisted and Eventlet reactors by
20+
using pyOpenSSL contexts with mapped protocol, verification, cipher, SNI, and
21+
hostname-validation settings. Nonempty options without ``cert_reqs`` now
22+
default to peer-certificate verification on these reactors instead of
23+
pyOpenSSL's ``VERIFY_NONE``. When ``ca_certs`` is omitted, the system trust
24+
roots are loaded. Serialized symbolic stdlib protocol names are supported;
25+
plain integer protocol values are mapped only when unambiguous and otherwise
26+
rejected rather than being silently interpreted as a different pyOpenSSL
27+
method. Existing configurations that connect to an untrusted certificate
28+
must supply the appropriate CA or explicitly opt out with
29+
``cert_reqs=ssl.CERT_NONE`` and leave ``check_hostname`` disabled.
30+
* Hostname verification is now performed consistently when ``check_hostname``
31+
is enabled. The flag was previously not propagated to some connection paths,
32+
so hostname mismatches could be silently accepted; such mismatches now fail
33+
the connection. For a supplied standard-library context, enabling hostname
34+
checks also enables peer-certificate verification when necessary.
35+
* ``SSLContext`` setup now applies ``cert_reqs`` through ``verify_mode`` instead
36+
of overwriting the SSL options bitmask, preserving default hardening flags.
37+
1038
Others
1139
------
1240
* ``PreparedStatement.result_metadata`` and ``PreparedStatement.result_metadata_id`` are

cassandra/client_routes.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,29 @@ class _ClientRoutesHandler:
213213

214214
config: 'ClientRoutesConfig'
215215
ssl_enabled: bool
216+
endpoint_ssl_options: Optional[Dict]
216217
_routes: _RouteStore
217218
_connection_ids: Set[str]
218219
_proxy_addresses_override: Dict[str, str]
219220

220-
def __init__(self, config: 'ClientRoutesConfig', ssl_enabled: bool = False):
221+
def __init__(self, config: 'ClientRoutesConfig', ssl_enabled: bool = False,
222+
endpoint_ssl_options: Optional[Dict] = None):
221223
"""
222224
:param config: ClientRoutesConfig instance
223225
:param ssl_enabled: Whether TLS is enabled (determines port selection)
226+
:param endpoint_ssl_options: SSL options inherited from endpoint
227+
contact points for generated client-routes endpoints
224228
"""
225229
if not isinstance(config, ClientRoutesConfig):
226230
raise TypeError("config must be a ClientRoutesConfig instance")
227231

228232
self.config = config
229233
self.ssl_enabled = ssl_enabled
234+
self.endpoint_ssl_options = (
235+
dict(endpoint_ssl_options)
236+
if endpoint_ssl_options is not None
237+
else None
238+
)
230239
self._routes = _RouteStore()
231240
self._connection_ids = {dep.connection_id for dep in config.proxies}
232241
# Precalculate proxy address mappings for efficient lookup

cassandra/cluster.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import re
3838
import queue
3939
import socket
40+
import ssl
4041
import sys
4142
import time
4243
from threading import Lock, RLock, Thread, Event
@@ -883,12 +884,29 @@ def default_retry_policy(self, policy):
883884
when new sockets are created. This should be used when client encryption is enabled
884885
in Cassandra.
885886
886-
The following documentation only applies when ssl_options is used without ssl_context.
887-
888-
By default, a ``ca_certs`` value should be supplied (the value should be
889-
a string pointing to the location of the CA certs file), and you probably
890-
want to specify ``ssl_version`` as ``ssl.PROTOCOL_TLS`` to match
891-
Cassandra's default protocol.
887+
The following documentation only applies when cluster-level ``ssl_options``
888+
is used without ``ssl_context``. An :class:`~cassandra.connection.EndPoint`
889+
may independently supply SSL options for its connections.
890+
891+
.. versionchanged:: 3.29.12
892+
893+
An explicit empty dict (``ssl_options={}``) enables TLS with default
894+
options. Cluster-level ``ssl_options=None`` does not enable TLS by
895+
itself; an ``ssl_context`` or SSL options supplied by the selected
896+
:class:`~cassandra.connection.EndPoint` still enable it. Empty options
897+
do not verify the server certificate unless the endpoint supplies
898+
additional security settings. A nonempty options dict enables
899+
peer-certificate verification by default unless ``cert_reqs`` explicitly
900+
disables it. Setting
901+
``'check_hostname': True`` always enables peer-certificate verification
902+
because hostname checking requires an authenticated certificate chain.
903+
When verification is enabled and ``ca_certs`` is omitted, the system
904+
trust roots are loaded.
905+
906+
Supply ``ca_certs`` as a path to a CA certificate file when the server uses
907+
a private CA that is not in the system trust store. You probably also want
908+
to specify ``ssl_version`` as ``ssl.PROTOCOL_TLS`` to match Cassandra's
909+
default protocol.
892910
893911
.. versionchanged:: 3.3.0
894912
@@ -1298,7 +1316,8 @@ def __init__(self,
12981316

12991317
if cloud is not None:
13001318
self.cloud = cloud
1301-
if contact_points is not _NOT_SET or endpoint_factory or ssl_context or ssl_options:
1319+
if (contact_points is not _NOT_SET or endpoint_factory or
1320+
ssl_context is not None or ssl_options is not None):
13021321
raise ValueError("contact_points, endpoint_factory, ssl_context, and ssl_options "
13031322
"cannot be specified with a cloud configuration")
13041323

@@ -1345,6 +1364,13 @@ def __init__(self,
13451364
if not isinstance(client_routes_config, ClientRoutesConfig):
13461365
raise TypeError("client_routes_config must be a ClientRoutesConfig instance")
13471366

1367+
endpoint_ssl_options = next((
1368+
cp.ssl_options
1369+
for cp in self.contact_points
1370+
if (isinstance(cp, EndPoint) and
1371+
cp.ssl_options is not None)
1372+
), None)
1373+
13481374
# SSL hostname verification is incompatible with client routes:
13491375
# connections go through NLB proxies whose addresses won't match
13501376
# server certificates.
@@ -1353,6 +1379,9 @@ def __init__(self,
13531379
_check_hostname_enabled = True
13541380
if ssl_options is not None and ssl_options.get('check_hostname', False):
13551381
_check_hostname_enabled = True
1382+
if (endpoint_ssl_options is not None and
1383+
endpoint_ssl_options.get('check_hostname', False)):
1384+
_check_hostname_enabled = True
13561385
if _check_hostname_enabled:
13571386
raise ValueError(
13581387
"SSL hostname verification (check_hostname=True) is currently incompatible "
@@ -1362,8 +1391,16 @@ def __init__(self,
13621391
"ssl_context.check_hostname = False."
13631392
)
13641393

1365-
ssl_enabled = ssl_context is not None or ssl_options is not None
1366-
self._client_routes_handler = _ClientRoutesHandler(client_routes_config, ssl_enabled=ssl_enabled)
1394+
ssl_enabled = (
1395+
ssl_context is not None or
1396+
ssl_options is not None or
1397+
endpoint_ssl_options is not None
1398+
)
1399+
self._client_routes_handler = _ClientRoutesHandler(
1400+
client_routes_config,
1401+
ssl_enabled=ssl_enabled,
1402+
endpoint_ssl_options=endpoint_ssl_options,
1403+
)
13671404

13681405
if contact_points is _NOT_SET or not self._contact_points_explicit:
13691406
seed_addrs = [dep.connection_addr_override for dep in client_routes_config.proxies
@@ -1508,12 +1545,25 @@ def __init__(self,
15081545

15091546
self.metrics_enabled = metrics_enabled
15101547

1511-
if ssl_options and not ssl_context:
1548+
if ssl_options is not None and ssl_context is None:
15121549
warn('Using ssl_options without ssl_context is '
15131550
'deprecated and will result in an error in '
15141551
'the next major release. Please use ssl_context '
15151552
'to prepare for that release.',
15161553
DeprecationWarning)
1554+
if not ssl_options:
1555+
log.warning(
1556+
'Cluster-level ssl_options enable TLS without requesting '
1557+
'server certificate verification; endpoint-level SSL '
1558+
'options may enable verification for individual '
1559+
'connections')
1560+
elif (ssl_options.get('check_hostname', False) and
1561+
ssl_options.get('cert_reqs') == ssl.CERT_NONE):
1562+
log.warning(
1563+
'Cluster-level check_hostname=True requires server '
1564+
'certificate verification and overrides '
1565+
'cert_reqs=CERT_NONE at cluster level; endpoint-level SSL '
1566+
'options may override these settings')
15171567

15181568
self.ssl_options = ssl_options
15191569
self.ssl_context = ssl_context

0 commit comments

Comments
 (0)