Skip to content

connection: fix ssl_options TLS handling - #938

Open
dkropachev wants to merge 1 commit into
masterfrom
fix-empty-ssl-options-reactors
Open

connection: fix ssl_options TLS handling#938
dkropachev wants to merge 1 commit into
masterfrom
fix-empty-ssl-options-reactors

Conversation

@dkropachev

@dkropachev dkropachev commented Jul 20, 2026

Copy link
Copy Markdown

Summary

Fixes #937: explicit ssl_options={} was treated like omitted ssl_options=None because several paths checked ssl_options truthiness.

This PR preserves whether ssl_options was supplied and fixes the reactor paths needed to use that state consistently:

  • ssl_options={} enables TLS with default options.
  • ssl_options=None remains plaintext unless endpoint SSL options are supplied.
  • Endpoint SSL options mark connection, cluster, Insights, and shard-aware paths as SSL-enabled.
  • Legacy ssl_options build compatible pyOpenSSL contexts for Eventlet/Twisted with protocol, verification, certificate, cipher, SNI, and hostname-validation handling.
  • Non-empty legacy options default to peer verification and load system trust roots when ca_certs is omitted.
  • pyOpenSSL certificates use typed cryptography SAN/CN parsing, including compatibility with pyOpenSSL 26.2+, which removed X509.get_extension().

Public Cluster API and protocol format are unchanged.

In Scope

  • Connection SSL state tracking via explicit/omitted ssl_options.
  • Default asyncio/Eventlet/Twisted paths using the normalized SSL-enabled state.
  • Cluster warning/validation behavior for explicit empty SSL options.
  • Shard-aware port selection for SSL-enabled configurations.
  • Client-routes SSL mode checks.
  • Insights startup reporting for SSL-enabled and plaintext connections.
  • Shared pyOpenSSL context construction and typed certificate parsing needed by Eventlet/Twisted.
  • Cloud pyOpenSSL context method selection so reactor behavior remains consistent.
  • Documentation and changelog for the new SSL and hostname-verification behavior.

Compatibility and Protocol Risk

No protocol changes.

An explicit empty dict now enables encrypted traffic without server certificate verification. ssl_options=None remains plaintext unless endpoint SSL options are supplied.

Non-empty legacy options now default to peer-certificate verification. On Eventlet/Twisted this changes pyOpenSSL's prior VERIFY_NONE default to VERIFY_PEER; when ca_certs is omitted, the driver loads system trust roots. Configurations using a private or self-signed CA must provide ca_certs, or explicitly use cert_reqs=ssl.CERT_NONE with hostname checking disabled.

Hostname verification is now actually enforced when check_hostname is enabled. A caller-supplied pyOpenSSL context that lacks peer verification is promoted to VERIFY_PEER; this mutation is logged and replaces its verification callback. Contexts already using VERIFY_PEER retain their existing callback.

Follow-up PR

Follow-up #941 remains for additional pyOpenSSL parity and client-routes compatibility with caller-supplied pyOpenSSL-style contexts. Typed SAN/CN parsing moved into this PR because current pyOpenSSL releases have already removed the deprecated extension API.

Tests

  • uv run pytest -rf tests/unit/test_connection.py tests/unit/test_cluster.py tests/unit/test_client_routes.py tests/unit/test_cloud.py tests/unit/test_shard_aware.py tests/unit/advanced/test_insights.py tests/unit/io/test_asyncioreactor.py tests/unit/io/test_eventletreactor.py tests/unit/io/test_twistedreactor.py — 211 passed, 5 skipped.
  • TZ=UTC uv run pytest -rf tests/unit — 799 passed, 46 skipped.
  • pyOpenSSL 26.2.0 and 26.3.0 real-X509 regression runs — passed with X509.get_extension() absent.
  • make -C docs test — passed with warnings treated as errors.

Integration scenario to consider: connect to a TLS cluster with Cluster(..., ssl_options={}) under the default, Twisted, and Eventlet reactors.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change distinguishes omitted SSL options from explicitly supplied options, including {}, across connection initialization, asyncio, Eventlet, and Twisted reactors. It adds shared pyOpenSSL context and hostname validation, updates Insights reporting and shard-aware SSL routing, adjusts cloud TLS method selection and cluster validation, and adds focused tests.

Sequence Diagram(s)

sequenceDiagram
  participant Cluster
  participant Connection
  participant Reactor
  participant pyOpenSSL
  Cluster->>Connection: provide ssl_options or ssl_context
  Connection->>Reactor: expose normalized SSL state
  Reactor->>pyOpenSSL: build context and perform handshake
  pyOpenSSL-->>Reactor: provide peer certificate
  Reactor->>Connection: validate certificate hostname
Loading

Possibly related PRs

Suggested labels: area/Driver_-_python-driver

Suggested reviewers: lorak-mmk, nikagra, sylwiaszunejko

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes cloud, client-routes, shard-aware, and cluster validation code that is not required by #937's reactor/Insights scope. Move the extra SSL-related follow-ups into separate PRs or add linked issues that explicitly cover the broader scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the requested explicit-empty-vs-omitted ssl_options behavior across connection, reactors, and Insights.
Title check ✅ Passed The title clearly matches the main change: fixing TLS handling for ssl_options, especially explicit empty options.
Description check ✅ Passed The description is detailed and covers summary, scope, risks, follow-up, and tests, though the checklist items aren't explicitly checked.

Comment @coderabbitai help to get the list of available commands.

@dkropachev
dkropachev force-pushed the fix-empty-ssl-options-reactors branch from 704dde9 to 805b678 Compare July 20, 2026 19:15
@coderabbitai
coderabbitai Bot requested a review from Lorak-mmk July 20, 2026 19:16
Comment thread cassandra/io/twistedreactor.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cassandra/io/eventletreactor.py (1)

121-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead code: uses_legacy_ssl_options is now always False.

Since __init__ hardcodes self.uses_legacy_ssl_options = False and nothing else ever sets it True, the if self.uses_legacy_ssl_options: super()... branches in _initiate_connection and _validate_hostname are now unreachable. TwistedConnection doesn't carry this flag at all and branches on _ssl_enabled directly — consider removing the vestigial flag/branches here for consistency and to avoid implying conditional behavior that no longer exists.

♻️ Suggested cleanup
     def __init__(self, *args, **kwargs):
         Connection.__init__(self, *args, **kwargs)
-        self.uses_legacy_ssl_options = False
         self._write_queue = Queue()
...
     def _initiate_connection(self, sockaddr):
-        if self.uses_legacy_ssl_options:
-            super(EventletConnection, self)._initiate_connection(sockaddr)
-        else:
-            self._socket.connect(sockaddr)
-            if self._ssl_enabled:
-                self._socket.do_handshake()
+        self._socket.connect(sockaddr)
+        if self._ssl_enabled:
+            self._socket.do_handshake()

     def _validate_hostname(self):
-        if self.uses_legacy_ssl_options:
-            super(EventletConnection, self)._validate_hostname()
-        else:
-            expected_name = (self.ssl_options or {}).get('server_hostname') or self.endpoint.address
-            _validate_pyopenssl_hostname(self._socket.get_peer_certificate(), expected_name)
+        expected_name = (self.ssl_options or {}).get('server_hostname') or self.endpoint.address
+        _validate_pyopenssl_hostname(self._socket.get_peer_certificate(), expected_name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cassandra/io/eventletreactor.py` around lines 121 - 158, Remove the hardcoded
uses_legacy_ssl_options assignment from EventletConnection.__init__ and delete
the unreachable legacy branches in _initiate_connection and _validate_hostname.
Keep the current non-legacy socket connection, handshake, and hostname
validation behavior as the unconditional implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cassandra/io/eventletreactor.py`:
- Around line 121-158: Remove the hardcoded uses_legacy_ssl_options assignment
from EventletConnection.__init__ and delete the unreachable legacy branches in
_initiate_connection and _validate_hostname. Keep the current non-legacy socket
connection, handshake, and hostname validation behavior as the unconditional
implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f288eee-0f6d-48f6-b321-96aa8ca19b32

📥 Commits

Reviewing files that changed from the base of the PR and between bcc2d3d and 805b678.

📒 Files selected for processing (13)
  • cassandra/cluster.py
  • cassandra/connection.py
  • cassandra/datastax/insights/reporter.py
  • cassandra/io/asyncioreactor.py
  • cassandra/io/eventletreactor.py
  • cassandra/io/twistedreactor.py
  • cassandra/pool.py
  • tests/unit/advanced/test_insights.py
  • tests/unit/io/test_eventletreactor.py
  • tests/unit/io/test_twistedreactor.py
  • tests/unit/test_client_routes.py
  • tests/unit/test_connection.py
  • tests/unit/test_shard_aware.py

@dkropachev dkropachev self-assigned this Jul 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cassandra/datastax/cloud/__init__.py (1)

188-193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use raise ... from for cleaner exception chaining.

Within an except clause, using raise ... from e is the idiomatic Python 3 approach. It automatically preserves the original traceback and sets the __cause__ attribute, making it cleaner than manually chaining with .with_traceback().
As per static analysis hints, within an except clause, exceptions should be raised with raise ... from err to distinguish them from errors in exception handling.

♻️ Proposed refactor
     try:
         from OpenSSL import SSL
     except ImportError as e:
         raise ImportError(
-            "PyOpenSSL must be installed to connect to Astra with the Eventlet or Twisted event loops")\
-            .with_traceback(e.__traceback__)
+            "PyOpenSSL must be installed to connect to Astra with the Eventlet or Twisted event loops"
+        ) from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cassandra/datastax/cloud/__init__.py` around lines 188 - 193, Update the
OpenSSL import error handling in the cloud initialization code to raise the
custom ImportError using Python’s explicit exception chaining syntax with the
caught exception as its cause. Remove the manual .with_traceback() chaining
while preserving the existing error message and behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cassandra/datastax/cloud/__init__.py`:
- Around line 188-193: Update the OpenSSL import error handling in the cloud
initialization code to raise the custom ImportError using Python’s explicit
exception chaining syntax with the caught exception as its cause. Remove the
manual .with_traceback() chaining while preserving the existing error message
and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b73f8af-9ad3-478c-ae6a-58f2804a5f23

📥 Commits

Reviewing files that changed from the base of the PR and between 805b678 and 8e17dd5.

📒 Files selected for processing (7)
  • cassandra/datastax/cloud/__init__.py
  • cassandra/io/eventletreactor.py
  • cassandra/io/twistedreactor.py
  • tests/unit/io/test_eventletreactor.py
  • tests/unit/io/test_twistedreactor.py
  • tests/unit/test_cloud.py
  • tests/unit/test_cluster.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/unit/io/test_twistedreactor.py
  • cassandra/io/eventletreactor.py
  • cassandra/io/twistedreactor.py
  • tests/unit/io/test_eventletreactor.py

@dkropachev
dkropachev force-pushed the fix-empty-ssl-options-reactors branch from 8e17dd5 to f119103 Compare July 20, 2026 22:24
@dkropachev

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
cassandra/io/twistedreactor.py (1)

43-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate pyOpenSSL helpers across reactors and cloud module.

_default_ssl_method (43-48) is an exact duplicate of cassandra/datastax/cloud/__init__.py::_default_pyopenssl_ssl_method, and per the codebase graph, cassandra/io/eventletreactor.py defines an identical _default_ssl_method/_build_pyopenssl_context_from_options pair as well. Three independent copies of TLS negotiation/context-building logic increase the risk of divergence (e.g. one path missing a future security fix).

Extracting these into a single shared module (e.g. near _validate_pyopenssl_hostname in cassandra/connection.py) would remove the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cassandra/io/twistedreactor.py` around lines 43 - 80, Extract the shared
pyOpenSSL TLS negotiation and context-building logic from `_default_ssl_method`
and `_build_pyopenssl_context_from_options` into a common helper module,
alongside the existing shared SSL utilities such as
`_validate_pyopenssl_hostname`. Update `cassandra/io/twistedreactor.py`,
`cassandra/io/eventletreactor.py`, and `cassandra/datastax/cloud/__init__.py` to
import and reuse the shared helpers, preserving their current certificate,
verification, and fallback behavior.
cassandra/datastax/cloud/__init__.py (1)

179-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate TLS-method-selection helper across three modules.

This exact loop-and-fallback logic is duplicated verbatim in cassandra/io/twistedreactor.py (_default_ssl_method) and, per the codebase graph, cassandra/io/eventletreactor.py (_default_ssl_method). Three independent copies of security-relevant TLS negotiation logic risk silently diverging if one is patched without the others.

Consider hoisting this into a single shared helper (e.g. alongside _validate_pyopenssl_hostname in cassandra/connection.py) and having all three call sites import it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cassandra/datastax/cloud/__init__.py` around lines 179 - 184, Consolidate the
duplicated TLS method-selection loop from _default_pyopenssl_ssl_method,
twistedreactor._default_ssl_method, and eventletreactor._default_ssl_method into
one shared helper alongside _validate_pyopenssl_hostname in connection.py.
Update all three modules to import and call the shared helper, removing their
local implementations while preserving the existing method order and ImportError
fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/io/test_eventletreactor.py`:
- Around line 122-131: Update test_validate_hostname_rejects_mismatch and
test_validate_hostname_prefers_san_over_common_name to assert
ssl.CertificateError specifically when _validate_hostname rejects the
certificate, replacing the broad Exception assertion while preserving the
existing test setup and invocation.

---

Nitpick comments:
In `@cassandra/datastax/cloud/__init__.py`:
- Around line 179-184: Consolidate the duplicated TLS method-selection loop from
_default_pyopenssl_ssl_method, twistedreactor._default_ssl_method, and
eventletreactor._default_ssl_method into one shared helper alongside
_validate_pyopenssl_hostname in connection.py. Update all three modules to
import and call the shared helper, removing their local implementations while
preserving the existing method order and ImportError fallback.

In `@cassandra/io/twistedreactor.py`:
- Around line 43-80: Extract the shared pyOpenSSL TLS negotiation and
context-building logic from `_default_ssl_method` and
`_build_pyopenssl_context_from_options` into a common helper module, alongside
the existing shared SSL utilities such as `_validate_pyopenssl_hostname`. Update
`cassandra/io/twistedreactor.py`, `cassandra/io/eventletreactor.py`, and
`cassandra/datastax/cloud/__init__.py` to import and reuse the shared helpers,
preserving their current certificate, verification, and fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9c06e84-df9d-4864-bb9c-2a2a66a988d5

📥 Commits

Reviewing files that changed from the base of the PR and between 8e17dd5 and f119103.

📒 Files selected for processing (16)
  • cassandra/cluster.py
  • cassandra/connection.py
  • cassandra/datastax/cloud/__init__.py
  • cassandra/datastax/insights/reporter.py
  • cassandra/io/asyncioreactor.py
  • cassandra/io/eventletreactor.py
  • cassandra/io/twistedreactor.py
  • cassandra/pool.py
  • tests/unit/advanced/test_insights.py
  • tests/unit/io/test_eventletreactor.py
  • tests/unit/io/test_twistedreactor.py
  • tests/unit/test_client_routes.py
  • tests/unit/test_cloud.py
  • tests/unit/test_cluster.py
  • tests/unit/test_connection.py
  • tests/unit/test_shard_aware.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/unit/test_cloud.py
  • tests/unit/test_cluster.py
  • tests/unit/test_client_routes.py
  • cassandra/cluster.py
  • tests/unit/io/test_twistedreactor.py
  • cassandra/pool.py
  • tests/unit/advanced/test_insights.py
  • cassandra/io/eventletreactor.py
  • cassandra/connection.py

Comment thread tests/unit/io/test_eventletreactor.py Outdated
@dkropachev

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
coderabbitai Bot requested a review from Lorak-mmk July 21, 2026 15:09
Copilot AI review requested due to automatic review settings July 21, 2026 20:10
Comment thread tests/unit/io/test_eventletreactor.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Normalizes explicit empty ssl_options handling so {} enables TLS while None remains disabled.

Changes:

  • Preserves explicit SSL configuration across connections, reactors, routing, and Insights.
  • Builds default SSL contexts for standard, Eventlet, and Twisted paths.
  • Adds focused SSL behavior and compatibility tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
cassandra/connection.py Preserves explicit SSL state and builds contexts.
cassandra/cluster.py Corrects cloud validation and warnings.
cassandra/pool.py Updates shard-aware TLS port selection.
cassandra/io/asyncioreactor.py Uses normalized SSL state.
cassandra/io/eventletreactor.py Builds pyOpenSSL contexts for Eventlet.
cassandra/io/twistedreactor.py Builds pyOpenSSL contexts for Twisted.
cassandra/datastax/cloud/__init__.py Selects modern pyOpenSSL methods.
cassandra/datastax/insights/reporter.py Corrects SSL startup reporting.
tests/unit/test_connection.py Tests connection SSL semantics.
tests/unit/test_cluster.py Tests cloud conflicts and warnings.
tests/unit/test_cloud.py Tests pyOpenSSL method fallback.
tests/unit/test_client_routes.py Tests empty options with TLS routes.
tests/unit/test_shard_aware.py Tests shard-aware SSL ports.
tests/unit/io/test_eventletreactor.py Tests Eventlet SSL contexts.
tests/unit/io/test_twistedreactor.py Tests Twisted SSL contexts.
tests/unit/advanced/test_insights.py Tests Insights SSL reporting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cassandra/pool.py Outdated
Comment thread cassandra/io/eventletreactor.py Outdated
Comment thread cassandra/io/eventletreactor.py Outdated
Comment thread cassandra/io/eventletreactor.py Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 21:52
@dkropachev
dkropachev force-pushed the fix-empty-ssl-options-reactors branch from f8c3ca4 to 9bcedb1 Compare July 21, 2026 21:52
@dkropachev dkropachev changed the title connection: preserve explicit empty ssl_options connection: fix ssl_options TLS handling Jul 28, 2026
@dkropachev

Copy link
Copy Markdown
Author

Addressed the blocking items in b14fe37: raw integer cert_reqs mapping, missing peer certificate handling, Twisted fail-closed callback behavior, and Eventlet hostname error reporting. Also added Cluster.ssl_options docs, CHANGELOG entries, and updated the PR title/description. Deferring typed SAN parsing and cleanup-only items to #941. Validation: uv run pytest -rf tests/unit/test_connection.py tests/unit/test_cluster.py tests/unit/test_client_routes.py tests/unit/test_cloud.py tests/unit/test_shard_aware.py tests/unit/advanced/test_insights.py tests/unit/io/test_asyncioreactor.py tests/unit/io/test_eventletreactor.py tests/unit/io/test_twistedreactor.py -> 208 passed, 5 skipped.

@dkropachev
dkropachev requested a review from nikagra July 28, 2026 14:47
Comment thread cassandra/io/twistedreactor.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

CHANGELOG.rst:12

  • This newly advertised behavior conflicts with the SSL guide: docs/security.rst:50-53 says an ssl_context is required to enable SSL, and lines 70-72 say Twisted/Eventlet users must pass a pyOpenSSL context. Update that guide to document options-only TLS and the new reactor handling; otherwise the primary user documentation tells users this supported path is invalid.
* 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.

Copilot AI review requested due to automatic review settings July 28, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

cassandra/connection.py:185

  • When check_hostname=True is used with a supplied context from an older pyOpenSSL that lacks get_verify_mode(), this helper now silently does nothing. Such contexts default to VERIFY_NONE, so Eventlet/Twisted proceed to hostname-match a certificate whose chain was never authenticated; the new compatibility test codifies that insecure path. Please fail closed for this combination or otherwise force VERIFY_PEER while preserving any user verification callback.
def _ensure_pyopenssl_context_requires_verification(ssl_module, context, check_hostname):
    get_verify_mode = getattr(context, 'get_verify_mode', None)
    if (check_hostname and get_verify_mode is not None and
            get_verify_mode() == ssl_module.VERIFY_NONE):
        context.set_verify(
            ssl_module.VERIFY_PEER,
            callback=lambda _connection, _x509, _errnum, _errdepth, ok: ok
        )

@sylwiaszunejko

Copy link
Copy Markdown

Follow-up on the commit-shape review: I pushed the review fixes as a separate commit in 8d6fdc5. I agree the implementation commit is broad; I can split the original implementation commit before merge if we want to rewrite the PR history.

I think there is nothing wrong with rewriting PR history, if we would gain cleaner history on master. I think that is the case with this PR - one commit adding unnecessary noise, next one deleting it. It would be nice to have it cleaned before merge.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread cassandra/connection.py Outdated
@Lorak-mmk

Copy link
Copy Markdown

I think there is nothing wrong with rewriting PR history, if we would gain cleaner history on master. I think that is the case with this PR - one commit adding unnecessary noise, next one deleting it. It would be nice to have it cleaned before merge.

+1. This is easier to review than additional commits.
LLMs are reluctant to rewrite history, but it just means we need to nudge them, not give up.

@nikagra nikagra left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification pass on ab397328

I re-checked all 20 code threads against the code rather than the "Fixed in <sha>" replies, and exercised the pyOpenSSL paths at runtime with real OpenSSL.SSL objects. 19 of 20 are genuinely fixed — the stdlib→pyOpenSSL protocol/verify-mode mapping, the fail-closed info_callback, IP-literal handling, the shard-aware source-port bind, implicit SNI, and the per-connection callback all hold up under scrutiny. This latest force-push also dropped the wrapper indirection and the dead verify_callback, which removes two cleanup notes I had queued. Thanks for the turnaround on those.

Requesting changes on one item, plus two documentation gaps around genuinely breaking behaviour changes and some smaller cleanups.

Blocking: the hostname verifier cannot parse a real certificate on current pyOpenSSL

X509.get_extension was removed in pyOpenSSL 26.2.0. get_extension_count() survived, so _pyopenssl_cert_subject_alt_names enters the loop and then raises AttributeError for every certificate that carries extensions — which is every real server certificate.

Same code, same certificate bytes, only pyOpenSSL differing (certificates pulled from live TLS servers; full reproduction inline on cassandra/connection.py:272 and in the reopened thread):

                       pyOpenSSL 26.1.0    pyOpenSSL 26.3.0
  www.scylladb.com     VALIDATED           AttributeError: 'X509' object has no attribute 'get_extension'
  www.python.org       VALIDATED           AttributeError: 'X509' object has no attribute 'get_extension'
  github.com           VALIDATED           AttributeError: 'X509' object has no attribute 'get_extension'

This is only reachable because of this PR. On master, Connection._check_hostname is only ever the class attribute False — nothing in cassandra/ assigns it — so _validate_hostname(), asyncioreactor.py:228 and the check_hostname argument Twisted passes to _SSLCreator were all dead code. This PR is what turns them on, so check_hostname=True on Eventlet/Twisted moves from "silently not checked" to "cannot connect". That includes Cluster(cloud=...), which hard-codes ssl_options={'check_hostname': True} at cluster.py:1320.

Both reactors fail closed, so there is no MITM window — but TLS with hostname verification is unusable on them, and Eventlet surfaces it as an AttributeError out of Connection.__init__ rather than a connection error.

Why CI is green

tests/unit is fully green on the exact pyOpenSSL version where the feature is broken. Every hostname test injects a fake X509 double that implements get_extension itself, so no test ever touches a real OpenSSL.crypto.X509. That gap is what let a removed API through.

Environment

Python 3.12.3, pyOpenSSL 26.3.0, at ab397328:

  • tests/unit → 734 passed, 104 skipped
  • EVENT_LOOP_MANAGER=asyncio tests/unit/io/test_asyncioreactor.py → 3 passed
  • EventletSSLContextTest → 12 passed
  • TwistedSSLContextTest → 13 passed

Comment thread cassandra/connection.py Outdated
Comment thread cassandra/connection.py Outdated
Comment thread cassandra/connection.py Outdated
Comment thread cassandra/connection.py Outdated
Comment thread cassandra/datastax/insights/reporter.py Outdated
Comment thread cassandra/cluster.py
Comment thread tests/unit/test_connection.py Outdated
@dkropachev

Copy link
Copy Markdown
Author

All currently unresolved review feedback is addressed in 5ae31e1c7 and the nine review threads have fix-specific replies.

Additional documentation and validation completed:

  • docs/security.rst now documents options-only TLS, automatic pyOpenSSL contexts for Eventlet/Twisted, empty-option verification semantics, system trust roots, private CAs, and direct context types.
  • The targeted 10-file suite passed: 211 passed, 5 skipped.
  • Real-certificate hostname tests passed with pyOpenSSL 26.2 and 26.3, where X509.get_extension is removed.
  • Full unit suite passed under Python 3.13 with TZ=UTC: 799 passed, 46 skipped.
  • Sphinx docs validation passed with warnings treated as errors.
  • Cython generation and git diff --check passed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

cassandra/connection.py:203

  • When promoting a caller-supplied pyOpenSSL context to require peer verification, this overwrites the verify mode with VERIFY_PEER and can silently drop any other verification flags already set (e.g., VERIFY_FAIL_IF_NO_PEER_CERT or similar OpenSSL bitmask flags). To avoid changing behavior beyond adding peer verification (while still replacing the callback), compute the new mode by OR-ing the existing verify mode (when available) with VERIFY_PEER instead of replacing it entirely.
def _ensure_pyopenssl_context_requires_verification(ssl_module, context, check_hostname):
    """
    Make hostname verification fail closed for a caller-supplied context.

    When hostname checking is enabled, this may mutate the context's verify
    mode and callback because pyOpenSSL does not expose a non-mutating way to
    require peer verification.
    """
    if not check_hostname:
        return

    get_verify_mode = getattr(context, 'get_verify_mode', None)
    verify_mode = get_verify_mode() if get_verify_mode is not None else None
    if (verify_mode is not None and
            verify_mode & ssl_module.VERIFY_PEER):
        return

    log.warning(
        "check_hostname=True requires peer verification; mutating supplied "
        "pyOpenSSL context to use VERIFY_PEER and replacing its verification "
        "callback"
    )
    context.set_verify(
        ssl_module.VERIFY_PEER,
        callback=lambda _connection, _x509, _errnum, _errdepth, ok: ok
    )

tests/unit/test_shard_aware.py:78

  • This EndpointSSLOptionsEndPoint test helper is duplicated (also present in tests/unit/advanced/test_insights.py). Consider moving it to a shared test utility module (or a shared fixture) to prevent future drift and keep endpoint-SSL behavior tests consistent across suites.
class EndpointSSLOptionsEndPoint(DefaultEndPoint):
    @property
    def ssl_options(self):
        return {}

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

cassandra/connection.py:1173

  • This conditional expression is difficult to read and relies on operator-precedence rules that are easy to misinterpret. Refactor into an explicit if ssl_options is not None: ... else: ... (or add parentheses around the intended grouping) to make the verification-default logic unambiguous.
        self._ssl_options_verify_by_default = (
            bool(ssl_options) or endpoint_has_non_sni_options
            if ssl_options is not None
            else bool(endpoint_ssl_options)
        )

tests/unit/io/test_twistedreactor.py:37

  • These unit tests depend on a certificate file living under tests/integration/..., which can make the unit suite brittle in environments where integration assets aren’t present or packaged. Prefer generating a temporary CA bundle during the test (or patching Context.load_verify_locations and asserting the call) so unit tests remain hermetic and filesystem-independent.
CA_CERTS = os.path.abspath(os.path.join(
    os.path.dirname(__file__), '..', '..', 'integration', 'long', 'ssl', 'rootCa.crt'))

tests/unit/io/test_eventletreactor.py:38

  • Same issue as Twisted: the unit test suite is coupled to an integration certificate file on disk. Consider creating a temp CA cert file within the test (or mocking load_verify_locations) to avoid failures when integration fixtures are not available.
CA_CERTS = os.path.abspath(os.path.join(
    os.path.dirname(__file__), '..', '..', 'integration', 'long', 'ssl', 'rootCa.crt'))

tests/unit/io/test_eventletreactor.py:366

  • Sockets return bytes from recv() on Python 3; using a str here works only because the code checks falsiness, but it doesn’t reflect real behavior. Use b'' to better model actual socket/pyOpenSSL return values.
        conn = Mock(in_buffer_size=4096)
        conn._socket.recv.return_value = ''

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

tests/unit/test_shard_aware.py:78

  • This EndpointSSLOptionsEndPoint helper is duplicated (a very similar class also appears in tests/unit/advanced/test_insights.py). To reduce duplication and keep endpoint-test helpers consistent, consider moving this to a shared test utility module (e.g., tests/unit/io/utils.py) and importing it where needed.
class EndpointSSLOptionsEndPoint(DefaultEndPoint):
    @property
    def ssl_options(self):
        return {}

Comment thread cassandra/connection.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

@nikagra nikagra left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — verification pass on 08fc467c

This supersedes my earlier CHANGES_REQUESTED at 8d6fdc54 and ab397328. All 12 of my findings are fixed, and I checked them by running the code rather than by the Fixed in <sha> replies. Three of my threads were resolved without a reply; I verified those too and have confirmed each in its thread.

What I ran

Head 08fc467c in a clean worktree — Python 3.12.3, pyOpenSSL 26.3.0 (the release that removed X509.get_extension), cryptography 49.0.0, Twisted 26.4.0, eventlet 0.41.1:

  • TZ=UTC pytest tests/unit825 passed, 108 skipped. The skips are environmental in my setup and unrelated to this PR (48 libev, 16 asyncore, plus lz4/Cython/geomet/cmurmur3 absent). The tests this PR adds did run: test_connection.py, io/test_twistedreactor.py and io/test_eventletreactor.py are 146 passed / 2 skipped on their own.
  • Connection.__init__ SSL state matrix, 13 combinations against the real class: matches the documented behavior exactly. The caller's ssl_options dict is not mutated. Non-empty options load 121 system CA certs. OP_NO_SSLv3 survives — master's rv.options = int(cert_reqs) clobbering is genuinely gone.
  • Real TLS handshakes through _build_pyopenssl_context_from_options against a live TLS server: {} connects unverified; {'ciphers': 'HIGH'} without ca_certs fails with certificate verify failed; ca_certs verifies; symbolic and enum ssl_version both resolve; PROTOCOL_TLSv1_2 pins TLS 1.2.
  • Twisted end-to-end with check_hostname=True, real reactor and real cffi info callback: a matching name completes the handshake; a mismatched name fails with Hostname verification failed: hostname 'wrong.example.com' doesn't match certificate DNS subjectAltName ['localhost']; an untrusted CA fails with certificate verify failed. Fails closed.
  • Eventlet _connect_socket() against the same server: same four outcomes, and a mismatch surfaces as a clear ConnectionException rather than a socket error.
  • _validate_pyopenssl_hostname against OpenSSL X509_check_host semantics, 20 cases on real certificates: wildcards, IP-SAN-only, commonName-fallback suppression rules, IDNA-2003 alias rejection, *.2.3.4 against 1.2.3.4, absent certificate — all correct.
  • cassandra.datastax.cloud importing cassandra.connection at module scope: no circular import.

On the compatibility break

Non-empty legacy ssl_options without ca_certs now fails against a private CA on Twisted/Eventlet — I reproduced it (certificate verify failed). The CHANGELOG, docs/security.rst, and the Cluster.ssl_options docstring all state this plainly, which is the right handling for a change in this direction.

Non-blocking

Five inline comments, none of them merge conditions: two consistency points about when verification is on, one question about Astra under Twisted/Eventlet, and two small cleanups.

A few observations I'm not going to open threads for:

  • connection.py:287-289_pyopenssl_verify_mode_from_cert_reqs silently resolves the stdlib/pyOpenSSL integer collision (a genuine SSL.VERIFY_FAIL_IF_NO_PEER_CERT becomes VERIFY_PEER, dropping the flag), while _pyopenssl_ssl_method_from_stdlib raises on the same class of ambiguity. Both land on the safe side here; a one-line comment explaining the asymmetry would save the next reader the trip.
  • connection.py:1425-1426check_hostname=True silently wins over an explicit cert_reqs=CERT_NONE. Stdlib raises ValueError for that combination; a warn would be kinder than silence.
  • connection.py:315-319 — the mutation warning fires per connection, not once per context, so a caller-supplied insecure context logs it on every pool connection. Worth also noting in the docstring that a caller's own set_info_callback is replaced, not just their verify callback.
  • connection.py:1531-1533 — last-error-wins means a hostname failure on the first address can be masked by an ECONNREFUSED on the second, and hostname failures retry every remaining addrinfo entry pointlessly. Pre-existing shape, newly reachable by verification errors.
  • tests/unit/test_cloud.py uses bare pytest functions where the rest of tests/unit uses unittest classes, and it mocks out _build_pyopenssl_context_from_options, so it pins the call shape rather than the resulting context. Fine given pyOpenSSL is optional — just noting what it does and doesn't cover.

Test coverage

This is the part I'd hold up as the model: real in-process OpenSSL.crypto.X509 certificates instead of doubles, correct skipIf guards so a missing pyOpenSSL doesn't silently disable the plaintext timer tests, and a named regression test for each previously-flagged issue (test_supplied_context_without_get_verify_mode_promotes_to_verify_peer, test_info_callback_fails_closed_on_unexpected_exception, test_hostname_error_closes_socket_and_tries_next_address). The one gap left is integration, which is the inline question about Astra.

Comment thread cassandra/connection.py Outdated
Comment thread cassandra/connection.py Outdated
Comment thread cassandra/io/twistedreactor.py Outdated
Comment thread cassandra/io/twistedreactor.py Outdated
Comment thread tests/unit/test_cluster.py Outdated
Comment thread cassandra/tls.py Dismissed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

cassandra/datastax/insights/reporter.py:153

  • If the control connection disappears before startup data is collected, this fallback reports endpoint-only TLS as disabled because it checks only cluster-level fields. EndPoint contact points with non-None ssl_options are supported and now enable TLS, so the startup event can contradict the actual connection configuration during this race. Include endpoint-derived SSL state (or retain the last control-connection state) in this fallback.
            ssl_context = cluster.ssl_context
            ssl_options = cluster.ssl_options
            ssl_enabled = ssl_context is not None or ssl_options is not None

tests/unit/test_client_routes.py:460

  • This regression only covers cluster-level ssl_options={}. Supported EndPoint contact points can supply ssl_options={} themselves, but Cluster still initializes the client-routes handler from cluster-level settings only; it consequently selects port instead of tls_port, and generated ClientRoutesEndPoints do not retain the original endpoint SSL options. Add an endpoint-only case and propagate that TLS configuration so routed connections both select and use TLS.
            cluster = Cluster(
                contact_points=["10.0.0.1"],
                ssl_options={},
                client_routes_config=config,

Comment thread cassandra/connection.py
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

cassandra/datastax/insights/reporter.py:154

  • Because endpoint options are only read when cluster options are None, this fallback ignores them for explicit cluster options. For example, Cluster(ssl_options={}) plus an endpoint {'ca_certs': ...} creates a verifying context in Connection, but if the control connection is unavailable Insights reports certValidation: false from the empty cluster dict. Merge cluster and endpoint options with the same origin-sensitive verification default used by Connection before computing the fallback report.
            if ssl_options is None:
                ssl_options = next((

Comment thread cassandra/connection.py
Comment on lines +951 to +953
if (self.ssl_options.get('check_hostname', False) and
isinstance(self.ssl_context, ssl.SSLContext) and
not self.ssl_context.check_hostname):

@Lorak-mmk Lorak-mmk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it a single commit? There is no way for a human to read that.

Comment on lines +1 to +24
#!/usr/bin/env python
# Copyright DataStax, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Validate Astra bundle TLS hostname verification on a pyOpenSSL reactor.

Set ``ASTRA_SECURE_CONNECT_BUNDLE``, ``ASTRA_CLIENT_ID``, and
``ASTRA_CLIENT_SECRET``. Run once for each reactor:

uv run python scripts/validate_astra_tls.py twisted
uv run python scripts/validate_astra_tls.py eventlet

The script never prints credentials or bundle contents.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it datastax copyright? Why do we even do anything with astra? We never supported Datastax-specific functionality - we aim to remove it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Normalize explicit ssl_options={} handling across reactors and Insights

6 participants