Skip to content

Commit 042ba4e

Browse files
authored
Merge pull request #297 from consideRatio/pr/tls-handshake-error
Document configuring TLS ciphers and log a link to it on raised handshake error
2 parents b3843c9 + 56ceff6 commit 042ba4e

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,53 @@ JupyterHub create local accounts using the LDAPAuthenticator.
370370
Issue [#19](https://github.com/jupyterhub/ldapauthenticator/issues/19) provides
371371
additional discussion on local user creation.
372372

373+
## Handling SSL/TLS handshake errors
374+
375+
If you have received a SSL/TLS handshake error, it could be that no [cipher
376+
suite] accepted by LDAPAuthenticator is also accepted by the LDAP server. This
377+
is likely because LDAPAuthenticator is stricter than the LDAP server and only
378+
accepts modern cipher suites than the LDAP server doesn't accept. Due to this,
379+
you should from a security perspective ideally modernize the LDAP server's
380+
accepted cipher suites rather than expand the LDAPAuthenticator accepted cipher
381+
suites to include older cipher suites.
382+
383+
The cipher suites that LDAPAuthenticator accepted by default come from
384+
[ssl.create_default_context().get_ciphers()], which in turn can change with
385+
Python version. Upgrading Python from 3.7 - 3.9 to 3.10 - 3.13 is known to
386+
strictly reduce the set of accepted cipher suites from 30 to 17 for example. Due
387+
to this, upgrading Python could lead to observing a handshake error previously
388+
not observed.
389+
390+
If you want to configure LDAPAuthenticator to accept older cipher suites instead
391+
of updating the LDAP server to accept modern cipher suites, you can do it using
392+
`LDAPAuthenticator.tls_kwargs` as demonstrated below.
393+
394+
```python
395+
# default cipher suites accepted by LDAPAuthenticator in Python 3.7 - 3.9
396+
# it includes 30 cipher suites, where 13 of them were considered less secure
397+
# and removed as default cipher suites in Python 3.10
398+
old_ciphers_list_considered_less_secure = "AES128-SHA:AES256-SHA:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES256-GCM-SHA384:AES256-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CHACHA20-POLY1305:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
399+
400+
# default cipher suites accepted by LDAPAuthenticator in Python 3.10 - 3.13
401+
# this list includes 17 cipher suites out of the 30 in the old list, with no
402+
# new additions
403+
new_ciphers_list = "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CHACHA20-POLY1305:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
404+
405+
c.LDAPAuthenticator.tls_kwargs = {
406+
"ciphers": old_ciphers_list_considered_less_secure,
407+
}
408+
```
409+
410+
For reference, you can use a command like below to see what the default cipher
411+
suites LDAPAuthenticator will use in various Python versions.
412+
413+
```shell
414+
docker run -it --rm python:3.13 python -c 'import ssl; c = ssl.create_default_context(); print(":".join(sorted([c["name"] for c in c.get_ciphers()])))'
415+
```
416+
417+
[cipher suite]: https://en.wikipedia.org/wiki/Cipher_suite#Full_handshake:_coordinating_cipher_suites
418+
[ssl.create_default_context().get_ciphers()]: https://docs.python.org/3/library/ssl.html#ssl.create_default_context
419+
373420
## Testing LDAPAuthenticator without JupyterHub
374421

375422
This script can be written to a file such as `test_ldap_auth.py`, and run with

ldapauthenticator/ldapauthenticator.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import ldap3
66
from jupyterhub.auth import Authenticator
7-
from ldap3.core.exceptions import LDAPBindError
7+
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
88
from ldap3.core.tls import Tls
99
from ldap3.utils.conv import escape_filter_chars
1010
from ldap3.utils.dn import escape_rdn
@@ -536,6 +536,16 @@ def get_connection(self, userdn, password):
536536
password=password,
537537
auto_bind=auto_bind,
538538
)
539+
except LDAPSocketOpenError as e:
540+
if "handshake" in str(e).lower():
541+
self.log.error(
542+
"A TLS handshake failure has occurred. "
543+
"It could be an indication that no cipher suite accepted by "
544+
"LDAPAuthenticator was accepted by the LDAP server. For "
545+
"guidance on how to handle this, refer to documentation at "
546+
"https://github.com/consideRatio/ldapauthenticator/tree/main?tab=readme-ov-file#handling-ssltls-handshake-errors"
547+
)
548+
raise
539549
except LDAPBindError as e:
540550
self.log.debug(
541551
"Failed to bind {userdn}\n{e_type}: {e_msg}".format(

0 commit comments

Comments
 (0)