From 0327d41c6fd4799022f543773fe1b686005ebc8e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 5 Nov 2024 12:40:41 +0100 Subject: [PATCH 1/2] Document configuring TLS ciphers and log a link to it on raised handshake error --- README.md | 21 +++++++++++++++++++++ ldapauthenticator/ldapauthenticator.py | 13 ++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76aaba1..6c4fdac 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,27 @@ c.LDAPAuthenticator.tls_kwargs = { } ``` +If you have received a TLS handshake error, it could be that no cipher accepted +by LDAPAuthenticator is also accepted by the LDAP server. The default ciphers +accepted by LDAPAuthenticator is dependent on the Python version, where +upgrading to Python 3.10 is known to reduce the set of accepted ciphers. The +default list of ciphers stem from +[ssl.create_default_context().get_ciphers()](https://docs.python.org/3/library/ssl.html#ssl.create_default_context). + +To configure LDAPAuthenticator's accepted ciphers explicitly, you can do: + +```python +# default ciphers accepted with LDAPAuthenticator in Python < 3.10 +pre_python310_ciphers = "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" + +# default ciphers accepted with LDAPAuthenticator in Python >= 3.10 +post_python310_ciphers = "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" + +c.LDAPAuthenticator.tls_kwargs = { + "ciphers": pre_python310_ciphers, +} +``` + #### `LDAPAuthenticator.server_port` Port on which to contact the LDAP server. diff --git a/ldapauthenticator/ldapauthenticator.py b/ldapauthenticator/ldapauthenticator.py index 969cf02..5f720a8 100644 --- a/ldapauthenticator/ldapauthenticator.py +++ b/ldapauthenticator/ldapauthenticator.py @@ -4,7 +4,7 @@ import ldap3 from jupyterhub.auth import Authenticator -from ldap3.core.exceptions import LDAPBindError +from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError from ldap3.core.tls import Tls from ldap3.utils.conv import escape_filter_chars from ldap3.utils.dn import escape_rdn @@ -536,6 +536,17 @@ def get_connection(self, userdn, password): password=password, auto_bind=auto_bind, ) + except LDAPSocketOpenError as e: + if "handshake" in str(e).lower(): + self.log.error( + "A TLS handshake failure has occurred. " + "It could be an indication that no cipher accepted by " + "LDAPAuthenticator was accepted by the LDAP server. For " + "details on how to handle this, refer to documentation of " + "the tls_kwargs config on how to configure ciphers " + "https://github.com/jupyterhub/ldapauthenticator/#ldapauthenticatortls_kwargs." + ) + raise except LDAPBindError as e: self.log.debug( "Failed to bind {userdn}\n{e_type}: {e_msg}".format( From 56ceff63be438520bdb4be61b79c845d1bc412db Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 6 Nov 2024 10:49:26 +0100 Subject: [PATCH 2/2] Put handshake error docs in a dedicated README section --- README.md | 68 ++++++++++++++++++-------- ldapauthenticator/ldapauthenticator.py | 7 ++- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6c4fdac..9b92a33 100644 --- a/README.md +++ b/README.md @@ -210,27 +210,6 @@ c.LDAPAuthenticator.tls_kwargs = { } ``` -If you have received a TLS handshake error, it could be that no cipher accepted -by LDAPAuthenticator is also accepted by the LDAP server. The default ciphers -accepted by LDAPAuthenticator is dependent on the Python version, where -upgrading to Python 3.10 is known to reduce the set of accepted ciphers. The -default list of ciphers stem from -[ssl.create_default_context().get_ciphers()](https://docs.python.org/3/library/ssl.html#ssl.create_default_context). - -To configure LDAPAuthenticator's accepted ciphers explicitly, you can do: - -```python -# default ciphers accepted with LDAPAuthenticator in Python < 3.10 -pre_python310_ciphers = "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" - -# default ciphers accepted with LDAPAuthenticator in Python >= 3.10 -post_python310_ciphers = "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" - -c.LDAPAuthenticator.tls_kwargs = { - "ciphers": pre_python310_ciphers, -} -``` - #### `LDAPAuthenticator.server_port` Port on which to contact the LDAP server. @@ -391,6 +370,53 @@ JupyterHub create local accounts using the LDAPAuthenticator. Issue [#19](https://github.com/jupyterhub/ldapauthenticator/issues/19) provides additional discussion on local user creation. +## Handling SSL/TLS handshake errors + +If you have received a SSL/TLS handshake error, it could be that no [cipher +suite] accepted by LDAPAuthenticator is also accepted by the LDAP server. This +is likely because LDAPAuthenticator is stricter than the LDAP server and only +accepts modern cipher suites than the LDAP server doesn't accept. Due to this, +you should from a security perspective ideally modernize the LDAP server's +accepted cipher suites rather than expand the LDAPAuthenticator accepted cipher +suites to include older cipher suites. + +The cipher suites that LDAPAuthenticator accepted by default come from +[ssl.create_default_context().get_ciphers()], which in turn can change with +Python version. Upgrading Python from 3.7 - 3.9 to 3.10 - 3.13 is known to +strictly reduce the set of accepted cipher suites from 30 to 17 for example. Due +to this, upgrading Python could lead to observing a handshake error previously +not observed. + +If you want to configure LDAPAuthenticator to accept older cipher suites instead +of updating the LDAP server to accept modern cipher suites, you can do it using +`LDAPAuthenticator.tls_kwargs` as demonstrated below. + +```python +# default cipher suites accepted by LDAPAuthenticator in Python 3.7 - 3.9 +# it includes 30 cipher suites, where 13 of them were considered less secure +# and removed as default cipher suites in Python 3.10 +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" + +# default cipher suites accepted by LDAPAuthenticator in Python 3.10 - 3.13 +# this list includes 17 cipher suites out of the 30 in the old list, with no +# new additions +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" + +c.LDAPAuthenticator.tls_kwargs = { + "ciphers": old_ciphers_list_considered_less_secure, +} +``` + +For reference, you can use a command like below to see what the default cipher +suites LDAPAuthenticator will use in various Python versions. + +```shell +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()])))' +``` + +[cipher suite]: https://en.wikipedia.org/wiki/Cipher_suite#Full_handshake:_coordinating_cipher_suites +[ssl.create_default_context().get_ciphers()]: https://docs.python.org/3/library/ssl.html#ssl.create_default_context + ## Testing LDAPAuthenticator without JupyterHub This script can be written to a file such as `test_ldap_auth.py`, and run with diff --git a/ldapauthenticator/ldapauthenticator.py b/ldapauthenticator/ldapauthenticator.py index 5f720a8..78dc628 100644 --- a/ldapauthenticator/ldapauthenticator.py +++ b/ldapauthenticator/ldapauthenticator.py @@ -540,11 +540,10 @@ def get_connection(self, userdn, password): if "handshake" in str(e).lower(): self.log.error( "A TLS handshake failure has occurred. " - "It could be an indication that no cipher accepted by " + "It could be an indication that no cipher suite accepted by " "LDAPAuthenticator was accepted by the LDAP server. For " - "details on how to handle this, refer to documentation of " - "the tls_kwargs config on how to configure ciphers " - "https://github.com/jupyterhub/ldapauthenticator/#ldapauthenticatortls_kwargs." + "guidance on how to handle this, refer to documentation at " + "https://github.com/consideRatio/ldapauthenticator/tree/main?tab=readme-ov-file#handling-ssltls-handshake-errors" ) raise except LDAPBindError as e: