From 30566ff068188f8d122d450b4a4d3e4379f01c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Tue, 7 Jul 2026 18:14:08 +0200 Subject: [PATCH 1/2] fix(auth): prefer system CA bundle over capath On RHEL-family and other modern distributions, trusted CAs live in a PEM bundle (openssl_cafile) rather than a hashed capath directory. Using OPT_X_TLS_CACERTDIR with the default capath caused LDAPS/STARTTLS certificate validation to fail even for system-trusted CAs. Prefer OPT_X_TLS_CACERTFILE when the default bundle exists, and fall back to OPT_X_TLS_CACERTDIR otherwise. --- CHANGELOG.md | 5 ++- src/authentication/rfl/authentication/ldap.py | 36 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a14a33..9293e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,10 @@ and this project adheres to - log: Clear handlers in `setup_logger()` by default to avoid duplicate log lines, with optional clear=False to keep existing handlers (#88). -- auth: LDAP debug log search base error. +- auth: + - LDAP debug log search base error. + - LDAP TLS certificate validation with default system CA trust store by + preferring PEM bundle (CAFILE) over capath (CADIR). ## [1.8.0] - 2026-05-22 diff --git a/src/authentication/rfl/authentication/ldap.py b/src/authentication/rfl/authentication/ldap.py index e16963e..c022ca0 100644 --- a/src/authentication/rfl/authentication/ldap.py +++ b/src/authentication/rfl/authentication/ldap.py @@ -7,6 +7,7 @@ from typing import Optional, List, Tuple from pathlib import Path import logging +import os try: import ldap @@ -91,19 +92,34 @@ def connection(self): connection.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) # For LDAPS and STARTTLS, libldap require the path to CA certificates to be # defined to authentication server certificate. If the cacert option is - # defined, use it else use default system CA certificates directory defined - # in OpenSSL library. + # defined, use it else use default system CA certificates defined in + # OpenSSL library. Prefer the PEM bundle file (CAFILE) when available, + # as modern distributions store trust anchors there rather than in a + # hashed certificate directory (CADIR). if self.cacert is None: import ssl - logger.debug( - "Using default system OpenSSL CA certificate directory to " - "authenticate server" - ) - connection.set_option( - ldap.OPT_X_TLS_CACERTDIR, - ssl.get_default_verify_paths().openssl_capath, - ) + paths = ssl.get_default_verify_paths() + if paths.openssl_cafile and os.path.isfile(paths.openssl_cafile): + logger.debug( + "Using default system OpenSSL CA certificate file %s to " + "authenticate server", + paths.openssl_cafile, + ) + connection.set_option( + ldap.OPT_X_TLS_CACERTFILE, + paths.openssl_cafile, + ) + elif paths.openssl_capath and os.path.isdir(paths.openssl_capath): + logger.debug( + "Using default system OpenSSL CA certificate directory %s " + "to authenticate server", + paths.openssl_capath, + ) + connection.set_option( + ldap.OPT_X_TLS_CACERTDIR, + paths.openssl_capath, + ) else: logger.debug( "Using CA certification %s to authenticate server", self.cacert From 00d7f514629e56a6ad088bf51241465a7e103335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Tue, 7 Jul 2026 18:14:56 +0200 Subject: [PATCH 2/2] tests(auth) adapt tests to new CA file/path logic --- src/authentication/rfl/tests/test_ldap.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/authentication/rfl/tests/test_ldap.py b/src/authentication/rfl/tests/test_ldap.py index 001f31e..d11ee93 100644 --- a/src/authentication/rfl/tests/test_ldap.py +++ b/src/authentication/rfl/tests/test_ldap.py @@ -4,6 +4,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import os import unittest from unittest.mock import patch, Mock from pathlib import Path @@ -42,15 +43,21 @@ def test_connection_ssl_cert(self, mock_ldap): mock_ldap_object.set_option.assert_not_called() # With ldaps URI and no CA certificate path, check LDAP server certificate is - # required and validated with default system OpenSSL certificates directory. + # required and validated with default system OpenSSL CA certificates. self.authentifier.uri = urllib.parse.urlparse("ldaps://localhost") self.authentifier.connection() mock_ldap_object.set_option.assert_any_call( mock_ldap.OPT_X_TLS_REQUIRE_CERT, mock_ldap.OPT_X_TLS_DEMAND ) - mock_ldap_object.set_option.assert_any_call( - mock_ldap.OPT_X_TLS_CACERTDIR, ssl.get_default_verify_paths().openssl_capath - ) + paths = ssl.get_default_verify_paths() + if paths.openssl_cafile and os.path.isfile(paths.openssl_cafile): + mock_ldap_object.set_option.assert_any_call( + mock_ldap.OPT_X_TLS_CACERTFILE, paths.openssl_cafile + ) + elif paths.openssl_capath and os.path.isdir(paths.openssl_capath): + mock_ldap_object.set_option.assert_any_call( + mock_ldap.OPT_X_TLS_CACERTDIR, paths.openssl_capath + ) mock_ldap_object.reset_mock() # With CA certificate path, check LDAP server certificate is required and