Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ Features
statements skip re-sending result metadata on EXECUTE, and the driver automatically
refreshes cached metadata when the server detects a schema change (DRIVER-153)

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

Others
------
* ``PreparedStatement.result_metadata`` and ``PreparedStatement.result_metadata_id`` are
Expand Down
11 changes: 10 additions & 1 deletion cassandra/client_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,20 +213,29 @@ class _ClientRoutesHandler:

config: 'ClientRoutesConfig'
ssl_enabled: bool
endpoint_ssl_options: Optional[Dict]
_routes: _RouteStore
_connection_ids: Set[str]
_proxy_addresses_override: Dict[str, str]

def __init__(self, config: 'ClientRoutesConfig', ssl_enabled: bool = False):
def __init__(self, config: 'ClientRoutesConfig', ssl_enabled: bool = False,
endpoint_ssl_options: Optional[Dict] = None):
"""
:param config: ClientRoutesConfig instance
:param ssl_enabled: Whether TLS is enabled (determines port selection)
:param endpoint_ssl_options: SSL options inherited from endpoint
contact points for generated client-routes endpoints
"""
if not isinstance(config, ClientRoutesConfig):
raise TypeError("config must be a ClientRoutesConfig instance")

self.config = config
self.ssl_enabled = ssl_enabled
self.endpoint_ssl_options = (
dict(endpoint_ssl_options)
if endpoint_ssl_options is not None
else None
)
self._routes = _RouteStore()
self._connection_ids = {dep.connection_id for dep in config.proxies}
# Precalculate proxy address mappings for efficient lookup
Expand Down
70 changes: 60 additions & 10 deletions cassandra/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import re
import queue
import socket
import ssl
import sys
import time
from threading import Lock, RLock, Thread, Event
Expand Down Expand Up @@ -883,12 +884,29 @@ def default_retry_policy(self, policy):
when new sockets are created. This should be used when client encryption is enabled
in Cassandra.

The following documentation only applies when ssl_options is used without ssl_context.

By default, a ``ca_certs`` value should be supplied (the value should be
a string pointing to the location of the CA certs file), and you probably
want to specify ``ssl_version`` as ``ssl.PROTOCOL_TLS`` to match
Cassandra's default protocol.
The following documentation only applies when cluster-level ``ssl_options``
is used without ``ssl_context``. An :class:`~cassandra.connection.EndPoint`
may independently supply SSL options for its connections.

.. versionchanged:: 3.29.12
Comment thread
dkropachev marked this conversation as resolved.

An explicit empty dict (``ssl_options={}``) enables TLS with default
options. Cluster-level ``ssl_options=None`` does not enable TLS by
itself; an ``ssl_context`` or SSL options supplied by the selected
:class:`~cassandra.connection.EndPoint` still enable it. Empty options
do not verify the server certificate unless the endpoint supplies
additional security settings. A nonempty options dict enables
peer-certificate verification by default unless ``cert_reqs`` explicitly
disables it. Setting
``'check_hostname': True`` always enables peer-certificate verification
because hostname checking requires an authenticated certificate chain.
When verification is enabled and ``ca_certs`` is omitted, the system
trust roots are loaded.

Supply ``ca_certs`` as a path to a CA certificate file when the server uses
a private CA that is not in the system trust store. You probably also want
to specify ``ssl_version`` as ``ssl.PROTOCOL_TLS`` to match Cassandra's
default protocol.

.. versionchanged:: 3.3.0

Expand Down Expand Up @@ -1298,7 +1316,8 @@ def __init__(self,

if cloud is not None:
self.cloud = cloud
if contact_points is not _NOT_SET or endpoint_factory or ssl_context or ssl_options:
if (contact_points is not _NOT_SET or endpoint_factory or
ssl_context is not None or ssl_options is not None):
raise ValueError("contact_points, endpoint_factory, ssl_context, and ssl_options "
"cannot be specified with a cloud configuration")

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

endpoint_ssl_options = next((
cp.ssl_options
for cp in self.contact_points
if (isinstance(cp, EndPoint) and
cp.ssl_options is not None)
), None)

# SSL hostname verification is incompatible with client routes:
# connections go through NLB proxies whose addresses won't match
# server certificates.
Expand All @@ -1353,6 +1379,9 @@ def __init__(self,
_check_hostname_enabled = True
if ssl_options is not None and ssl_options.get('check_hostname', False):
_check_hostname_enabled = True
if (endpoint_ssl_options is not None and
endpoint_ssl_options.get('check_hostname', False)):
_check_hostname_enabled = True
if _check_hostname_enabled:
raise ValueError(
"SSL hostname verification (check_hostname=True) is currently incompatible "
Expand All @@ -1362,8 +1391,16 @@ def __init__(self,
"ssl_context.check_hostname = False."
)

ssl_enabled = ssl_context is not None or ssl_options is not None
self._client_routes_handler = _ClientRoutesHandler(client_routes_config, ssl_enabled=ssl_enabled)
ssl_enabled = (
ssl_context is not None or
ssl_options is not None or
endpoint_ssl_options is not None
)
self._client_routes_handler = _ClientRoutesHandler(
client_routes_config,
ssl_enabled=ssl_enabled,
endpoint_ssl_options=endpoint_ssl_options,
)

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

self.metrics_enabled = metrics_enabled

if ssl_options and not ssl_context:
if ssl_options is not None and ssl_context is None:
warn('Using ssl_options without ssl_context is '
'deprecated and will result in an error in '
'the next major release. Please use ssl_context '
'to prepare for that release.',
DeprecationWarning)
if not ssl_options:
log.warning(
'Cluster-level ssl_options enable TLS without requesting '
'server certificate verification; endpoint-level SSL '
'options may enable verification for individual '
'connections')
elif (ssl_options.get('check_hostname', False) and
ssl_options.get('cert_reqs') == ssl.CERT_NONE):
log.warning(
'Cluster-level check_hostname=True requires server '
'certificate verification and overrides '
'cert_reqs=CERT_NONE at cluster level; endpoint-level SSL '
'options may override these settings')

self.ssl_options = ssl_options
self.ssl_context = ssl_context
Expand Down
Loading
Loading