Skip to content

Commit 48b683b

Browse files
authored
Add certifi fallback for missing default CA certificates (DataDog#21609)
### What does this PR do? <!-- A brief description of the change being made with this pull request. --> Adds a fallback to `certifi` when the no CA certificates have been found during SSL context creation. The fallback is tested only on Linux as on Windows the [system certificate stores](https://learn.microsoft.com/en-us/windows/win32/seccrypto/system-store-locations?redirectedfrom=MSDN) are loaded by default. ([source](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_default_certs)) ### Motivation <!-- What inspired you to submit this pull request? --> On MacOS, some hosts were experiencing the following SSL Error: ``` SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1010)')) ``` This could have multiple root causes, originating at OpenSSL build time or from environment variables. `certifi.where()` is [used by the `requests` library](https://github.com/psf/requests/blob/420d16bc7ef326f7b65f90e4644adc0f6a0e1d44/src/requests/utils.py#L64) and fixes this issue, therefore we can use it as a fallback. ### Review checklist (to be filled by reviewers) - [ ] Feature or bugfix MUST have appropriate tests (unit, integration, e2e) - [ ] Add the `qa/skip-qa` label if the PR doesn't need to be tested during QA. - [ ] If you need to backport this PR to another branch, you can add the `backport/<branch-name>` label to the PR and it will automatically open a backport PR once this one is merged
1 parent 0f96340 commit 48b683b

3 files changed

Lines changed: 38 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `certifi` fallback for missing default CA certificates

datadog_checks_base/datadog_checks/base/utils/tls.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ def _load_ca_certs(context, config):
6969
context.load_verify_locations(cafile=ca_cert, capath=None, cadata=None)
7070
else:
7171
context.load_default_certs(ssl.Purpose.SERVER_AUTH)
72+
if not context.get_ca_certs():
73+
LOGGER.warning(
74+
'No CA certificates loaded from system default paths. '
75+
'This may indicate misconfigured SSL_CERT_FILE or SSL_CERT_DIR environment variables. '
76+
'Falling back to certifi certificate bundle.'
77+
)
78+
try:
79+
import certifi
80+
81+
context.load_verify_locations(cafile=certifi.where())
82+
except (ImportError, FileNotFoundError) as e:
83+
LOGGER.error('Failed to load fallback certificates from certifi: %s', e)
7284
except FileNotFoundError:
7385
LOGGER.warning(
7486
'TLS CA certificate file not found: %s. Please check the `tls_ca_cert` configuration option.',

datadog_checks_base/tests/base/utils/http/test_tls_and_certs.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from datadog_checks.base.utils.http import RequestsWrapper
1212
from datadog_checks.base.utils.tls import TlsConfig
13+
from datadog_checks.dev.utils import ON_WINDOWS
1314

1415
pytestmark = [pytest.mark.unit]
1516

@@ -67,6 +68,30 @@ def test_request_cert_gets_read(self, options, expected_cert, expected_key):
6768
mock_load_cert_chain.assert_called_once()
6869
mock_load_cert_chain.assert_called_with(expected_cert, keyfile=expected_key, password=None)
6970

71+
@pytest.mark.skipif(ON_WINDOWS, reason="Windows uses the default store locations.")
72+
def test_bad_default_verify_paths(self, monkeypatch, caplog):
73+
'''The SSL default verify paths can be set incorrectly.'''
74+
bad_cert_file = "/nonexistent/path/to/ssl/cert.pem"
75+
bad_cert_dir = "/nonexistent/path/to/ssl/certs"
76+
monkeypatch.setenv("SSL_CERT_FILE", bad_cert_file)
77+
monkeypatch.setenv("SSL_CERT_DIR", bad_cert_dir)
78+
bad_ssl_paths = ssl.DefaultVerifyPaths(
79+
cafile="None",
80+
capath="None",
81+
openssl_cafile_env="SSL_CERT_FILE",
82+
openssl_capath_env="SSL_CERT_DIR",
83+
openssl_cafile=bad_cert_file,
84+
openssl_capath=bad_cert_dir,
85+
)
86+
with mock.patch("ssl.get_default_verify_paths", return_value=bad_ssl_paths):
87+
with mock.patch("requests.Session.get"):
88+
with caplog.at_level(logging.WARNING):
89+
http = RequestsWrapper({"tls_verify": True}, {})
90+
assert ssl.get_default_verify_paths() == bad_ssl_paths
91+
assert http.session.adapters["https://"].ssl_context.get_ca_certs() != []
92+
http.get("https://example.com")
93+
assert 'Falling back to certifi certificate bundle.' in caplog.text
94+
7095

7196
class TestIgnoreTLSWarning:
7297
def test_config_default(self):

0 commit comments

Comments
 (0)