Repository: dask/distributed

Issue:
Insecure cipher suites was found
<!-- Please do a quick search of existing issues to make sure that this has not been asked before. -->
**Environment:**

 - Dask version: 2022.2
 - Python version:3.7
 - Operating System: Linux
 - Install method (conda, pip, source): pip
 
**What happened:**
When tls_min_version is specified TLSv1_3, the list of cipher suites provided by Client Hello contains insecure cipher suites, such as `TLS_RSA_W_AES_256_CBC_SHA256` .

Client Hello provides the following cipher suites:
```txt
            Cipher Suites (31 suites)
                Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
                Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
                Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9)
                Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
                Cipher Suite: TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xccaa)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (0x006b)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (0x0067)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
                Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)
                Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
                Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA256 (0x003d)
                Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c)
                Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
                Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
                Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)
```

**What you expected to happen:**
Leaving only the cipher suite supported by tls 1.3.
```txt
            Cipher Suites (4 suites)
                Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
                Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
                Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
                Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)
```

**Anything else we need to know?:**
Maybe we can disable insecure cipher suites by adding judgment statements?
Add a judgment to the next line of line 255.
https://github.com/dask/distributed/blob/b119eb925ac39e46c7bdac2c97d1fa5c1697d2cd/distributed/security.py#L255
Such as，
```python
            if self.tls_min_version is not None:
                ctx.minimum_version = self.tls_min_version

                if self.tls_min_version == ssl.TLSVersion.TLSv1_3:
                    ctx.options |= ssl.OP_NO_TLSv1_2
                
            if self.tls_max_version is not None:
                ctx.maximum_version = self.
```



Discussion:
Thanks for your report. Is there any way for us to reproduce this?

I was trying to reproduce this with the code below

```python
import dask
from distributed.security import Security
from distributed.utils_test import get_cert
ca_file = get_cert("tls-ca-cert.pem")
cert1 = get_cert("tls-cert.pem")
key1 = get_cert("tls-key.pem")
keycert1 = get_cert("tls-key-cert.pem")

c = {
    "distributed.comm.tls.ca-file": ca_file,
    "distributed.comm.tls.scheduler.key": key1,
    "distributed.comm.tls.scheduler.cert": cert1,
    "distributed.comm.tls.worker.cert": keycert1,
    "distributed.comm.tls.min-version": 1.3,
}
dask.config.set(c)
sec = Security()
ciphers = sec.get_connection_args("scheduler")['ssl_context'].get_ciphers()
print({d['name'] for d in ciphers})
```

but did not get any insecure ciphers


cc @jcrist can you reproduce this or help out? xref https://github.com/dask/distributed/pull/5594
I just noticed that you're running on python3.7 and an older version `2022.2`. Can you try reproducing this on a more recent version? We're no longer supporting python3.7 and I would like to confirm this is not linked to the python version

Resolving patch:
diff --git a/distributed/security.py b/distributed/security.py
--- a/distributed/security.py
+++ b/distributed/security.py
@@ -1,18 +1,68 @@
+from __future__ import annotations
+
 import datetime
 import os
+import sys
 import tempfile
+import warnings
 
 try:
     import ssl
 except ImportError:
     ssl = None  # type: ignore
 
+try:
+    from ssl import OPENSSL_VERSION_INFO as _OPENSSL_VERSION_INFO
+except ImportError:
+    _OPENSSL_VERSION_INFO = (0, 0, 0, 0, 0)
+
 import dask
 from dask.widgets import get_template
 
 __all__ = ("Security",)
 
 
+if sys.version_info >= (3, 10) or _OPENSSL_VERSION_INFO >= (1, 1, 0, 7):
+    # The OP_NO_SSL* and OP_NO_TLS* become deprecated in favor of
+    # 'SSLContext.minimum_version' from Python 3.7 onwards, however
+    # this attribute is not available unless the ssl module is compiled
+    # with OpenSSL 1.1.0g or newer.
+    # https://docs.python.org/3.10/library/ssl.html#ssl.SSLContext.minimum_version
+    # https://docs.python.org/3.7/library/ssl.html#ssl.SSLContext.minimum_version
+
+    # these _set_mimimun_version and _set_maximum_version depend on the validation
+    # already performed in `Security._set_tls_version_field`,
+    # and that they only apply to freshly created ssl.SSLContext instances in
+    # _get_tls_context
+    def _set_minimum_version(ctx: ssl.SSLContext, version: ssl.TLSVersion) -> None:
+        ctx.minimum_version = version
+
+    def _set_maximum_version(ctx: ssl.SSLContext, version: ssl.TLSVersion) -> None:
+        ctx.maximum_version = version
+
+else:
+
+    def _set_minimum_version(ctx: ssl.SSLContext, version: ssl.TLSVersion) -> None:
+        # if the ctx.maximum_version attribute is unsupported then we can infer
+        # that TLS 1.3 is not supported.
+        # _set_tls_version_field enforces that version is TLSVersion.TLSv1_2,
+        # or TLSVersion.TLSv1_3
+        if version is not ssl.TLSVersion.TLSv1_2:
+            raise ValueError(f"Unsupported TLS/SSL version: {version!r}")
+        ctx.options |= (
+            ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+        )
+
+    def _set_maximum_version(ctx: ssl.SSLContext, version: ssl.TLSVersion) -> None:
+        # if the ctx.maximum_version attribute is unsupported then we can infer
+        # that TLSv1_3 is not supported.
+        # _set_tls_version_field enforces that version is TLSVersion.TLSv1_2,
+        # TLSVersion.TLSv1_3, or None
+        # _get_tls_context enforces that version is not None
+        if version is not ssl.TLSVersion.TLSv1_2:
+            raise ValueError(f"Unsupported TLS/SSL version: {version!r}")
+
+
 class Security:
     """Security configuration for a Dask cluster.
 
@@ -71,6 +121,12 @@ class Security:
     )
 
     def __init__(self, require_encryption=None, **kwargs):
+        if _OPENSSL_VERSION_INFO < (1, 1, 1):
+            warnings.warn(
+                f"support for {ssl.OPENSSL_VERSION} is deprecated,"
+                " and will be removed in a future release",
+                DeprecationWarning,
+            )
         extra = set(kwargs).difference(self.__slots__)
         if extra:
             raise TypeError("Unknown parameters: %r" % sorted(extra))
@@ -251,10 +307,11 @@ def _get_tls_context(self, tls, purpose):
             else:
                 ctx = ssl.create_default_context(purpose=purpose, cafile=ca)
 
-            if self.tls_min_version is not None:
-                ctx.minimum_version = self.tls_min_version
+            # the _set_tls_version_field method enforces that
+            # self.tls_min_version is TLSv1_2, or TLSv1_3
+            _set_minimum_version(ctx, self.tls_min_version)
             if self.tls_max_version is not None:
-                ctx.maximum_version = self.tls_max_version
+                _set_maximum_version(ctx, self.tls_max_version)
 
             cert_in_memory = "\n" in cert
             key_in_memory = key is not None and "\n" in key


Test patch:
diff --git a/distributed/tests/test_security.py b/distributed/tests/test_security.py
--- a/distributed/tests/test_security.py
+++ b/distributed/tests/test_security.py
@@ -110,6 +110,14 @@ def test_min_max_version_config_errors(field):
             sec = Security()
 
 
+def test_invalid_min_version_from_config_errors():
+    with dask.config.set({"distributed.comm.tls.min-version": None}):
+        with pytest.raises(
+            ValueError, match=r"tls_min_version=.* is not supported, expected one of .*"
+        ):
+            Security(tls_min_version=ssl.TLSVersion.MINIMUM_SUPPORTED)
+
+
 def test_kwargs():
     c = {
         "distributed.comm.tls.ca-file": "ca.pem",