Skip to content

Commit 49c4d94

Browse files
committed
docs: add ssl_options migration guide
1 parent 9256914 commit 49c4d94

3 files changed

Lines changed: 122 additions & 7 deletions

File tree

cassandra/cluster.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,10 +1760,11 @@ def _raise_ssl_options_migration_error():
17601760
raise ValueError(
17611761
"ssl_options is deprecated and can no longer configure TLS. "
17621762
"Create an ssl.SSLContext and pass it using ssl_context instead. "
1763-
"Migration: ca_certs -> SSLContext.load_verify_locations(); "
1764-
"certfile/keyfile -> SSLContext.load_cert_chain(); cert_reqs -> "
1765-
"SSLContext.verify_mode; check_hostname -> "
1766-
"SSLContext.check_hostname; ciphers -> SSLContext.set_ciphers()."
1763+
"For example, ca_certs maps to "
1764+
"SSLContext.load_verify_locations(), and certfile/keyfile map to "
1765+
"SSLContext.load_cert_chain(). Migration guide: "
1766+
"https://python-driver.docs.scylladb.com/stable/"
1767+
"security.html#ssl-options-migration"
17671768
)
17681769

17691770
def protocol_downgrade(self, host_endpoint, previous_version):

docs/security.rst

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,117 @@ To enable SSL with version 3.17.0 and higher, set :attr:`.Cluster.ssl_context` t
5151
``ssl.SSLContext`` instance. The legacy :attr:`.Cluster.ssl_options` argument remains
5252
in the API only to raise an error with migration guidance when it is used.
5353

54+
.. _ssl-options-migration:
55+
56+
Migrating from ``ssl_options``
57+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58+
59+
Replace the legacy options dictionary with a context configured through the
60+
standard-library ``ssl`` API. For example, replace:
61+
62+
.. code-block:: python
63+
64+
import ssl
65+
from cassandra.cluster import Cluster
66+
67+
cluster = Cluster(
68+
['node.example.com'],
69+
ssl_options={
70+
'ca_certs': '/path/to/rootca.pem',
71+
'cert_reqs': ssl.CERT_REQUIRED,
72+
'check_hostname': True,
73+
},
74+
)
75+
76+
with:
77+
78+
.. code-block:: python
79+
80+
context = ssl.create_default_context(cafile='/path/to/rootca.pem')
81+
context.check_hostname = True
82+
83+
cluster = Cluster(
84+
['node.example.com'],
85+
ssl_context=context,
86+
)
87+
88+
Use the following mappings for other legacy options:
89+
90+
.. list-table:: ``ssl_options`` migration reference
91+
:header-rows: 1
92+
:widths: 22 38 40
93+
94+
* - Legacy option
95+
- Standard-library context
96+
- Twisted/Eventlet pyOpenSSL context
97+
* - ``ca_certs``
98+
- ``context.load_verify_locations(path)``
99+
- ``context.load_verify_locations(path)``
100+
* - ``certfile`` and ``keyfile``
101+
- ``context.load_cert_chain(certfile, keyfile, password)``
102+
- ``context.use_certificate_file(certfile)`` and
103+
``context.use_privatekey_file(keyfile)``
104+
* - ``cert_reqs``
105+
- ``context.verify_mode``
106+
- ``context.set_verify(mode, callback)``
107+
* - ``check_hostname``
108+
- ``context.check_hostname``
109+
- There is no direct context attribute. Configure hostname verification
110+
in a vetted pyOpenSSL verification layer, or migrate to a
111+
standard-library-backed reactor when hostname verification is required.
112+
* - ``ciphers``
113+
- ``context.set_ciphers(value)``
114+
- ``context.set_cipher_list(value.encode('ascii'))``
115+
* - ``ssl_version``
116+
- Start with ``ssl.PROTOCOL_TLS_CLIENT`` and configure
117+
``minimum_version`` and ``maximum_version`` when pinning is required.
118+
- Start with ``SSL.TLS_CLIENT_METHOD`` and configure protocol bounds on
119+
the context when pinning is required.
120+
* - ``server_hostname``
121+
- The driver derives this from the endpoint. Use
122+
:class:`~cassandra.connection.SniEndPoint` when explicit SNI routing is
123+
required.
124+
- The driver derives this from the endpoint. Use
125+
:class:`~cassandra.connection.SniEndPoint` when explicit SNI routing is
126+
required.
127+
* - ``server_side``, ``do_handshake_on_connect``, and
128+
``suppress_ragged_eofs``
129+
- No migration. The driver owns client-side socket creation and TLS
130+
handshakes; these low-level overrides are no longer configurable.
131+
- No migration. The driver owns client-side socket creation and TLS
132+
handshakes; these low-level overrides are no longer configurable.
133+
134+
An empty ``ssl_options={}`` previously left the intended verification policy
135+
ambiguous. Replace it with an explicit context. For an insecure development
136+
connection with no certificate verification:
137+
138+
.. code-block:: python
139+
140+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
141+
context.check_hostname = False
142+
context.verify_mode = ssl.CERT_NONE
143+
cluster = Cluster(['127.0.0.1'], ssl_context=context)
144+
145+
Do not use this configuration in production. Prefer
146+
``ssl.create_default_context()`` with a trusted CA and hostname verification.
147+
148+
For mutual TLS, load the client certificate and private key on the context:
149+
150+
.. code-block:: python
151+
152+
context = ssl.create_default_context(cafile='/path/to/rootca.pem')
153+
context.load_cert_chain(
154+
certfile='/path/to/client.crt',
155+
keyfile='/path/to/client.key',
156+
password='optional-key-password',
157+
)
158+
cluster = Cluster(['node.example.com'], ssl_context=context)
159+
160+
Twisted and Eventlet require an ``OpenSSL.SSL.Context`` instead of a standard
161+
``ssl.SSLContext``. See `SSL with Twisted or Eventlet`_ below for a complete
162+
example. Cloud secure-connect bundles require no migration; the driver creates
163+
their TLS context internally.
164+
54165
If you create your SSLContext using `ssl.create_default_context <https://docs.python.org/3/library/ssl.html#ssl.create_default_context>`_,
55166
be aware that SSLContext.check_hostname is set to True by default, so hostname validation is done
56167
by Python rather than the driver. The driver uses the endpoint address as the TLS server name.

tests/unit/test_cluster.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,21 +155,24 @@ def test_ssl_options_is_rejected(self):
155155
with self.subTest(ssl_options=ssl_options):
156156
with self.assertRaisesRegex(
157157
ValueError,
158-
"ssl_options is deprecated.*ssl_context.*ca_certs"):
158+
"ssl_options is deprecated.*ssl_context.*"
159+
"ssl-options-migration"):
159160
Cluster(ssl_options=ssl_options)
160161

161162
def test_ssl_options_is_rejected_with_ssl_context(self):
162163
with self.assertRaisesRegex(
163164
ValueError,
164-
"ssl_options is deprecated.*ssl_context.*ca_certs"):
165+
"ssl_options is deprecated.*ssl_context.*"
166+
"ssl-options-migration"):
165167
Cluster(ssl_options={}, ssl_context=Mock())
166168

167169
def test_ssl_options_assignment_is_rejected(self):
168170
cluster = Cluster()
169171

170172
with self.assertRaisesRegex(
171173
ValueError,
172-
"ssl_options is deprecated.*ssl_context.*ca_certs"):
174+
"ssl_options is deprecated.*ssl_context.*"
175+
"ssl-options-migration"):
173176
cluster.ssl_options = {'ca_certs': '/path/to/ca.pem'}
174177

175178
assert cluster.ssl_options is None

0 commit comments

Comments
 (0)