3737import re
3838import queue
3939import socket
40+ import ssl
4041import sys
4142import time
4243from 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