From 85a9881e4fe9de757d8973a4bfc4a691d390dbcd Mon Sep 17 00:00:00 2001 From: pierreln-dd Date: Tue, 23 Jun 2026 14:28:18 +0200 Subject: [PATCH 1/3] mongo: auto-enable TLS for MongoDB Atlas hosts MongoDB Atlas mandates TLS on all connections. Without it the driver surfaces a misleading "connection closed" error rather than a TLS failure. Auto-detect Atlas by checking configured hosts for the `mongodb.net` domain and enable TLS transparently, while preserving explicit `tls: false` overrides. Co-Authored-By: Claude Sonnet 4.6 --- mongo/datadog_checks/mongo/config.py | 16 ++++++++++++++-- mongo/tests/test_unit_config.py | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/mongo/datadog_checks/mongo/config.py b/mongo/datadog_checks/mongo/config.py index a2b244ef259df..456cc71843c0b 100644 --- a/mongo/datadog_checks/mongo/config.py +++ b/mongo/datadog_checks/mongo/config.py @@ -18,15 +18,27 @@ def __init__(self, instance, log, init_config): # x.509 authentication + # Auto-enable TLS for MongoDB Atlas: Atlas mandates TLS and the driver surfaces a + # misleading "connection closed" error when tls is omitted rather than a TLS error. + tls = instance.get('tls') + if tls is None: + raw_hosts = instance.get('hosts', []) + if isinstance(raw_hosts, str): + raw_hosts = [raw_hosts] + server = instance.get('server', '') or '' + if any('mongodb.net' in str(h) for h in raw_hosts) or 'mongodb.net' in server: + tls = True + log.debug('Auto-enabling TLS: detected MongoDB Atlas host (mongodb.net)') + cacert_cert_dir = instance.get('tls_ca_file') if cacert_cert_dir is None and ( - is_affirmative(instance.get('options', {}).get("tls")) or is_affirmative(instance.get('tls')) + is_affirmative(instance.get('options', {}).get("tls")) or is_affirmative(tls) ): cacert_cert_dir = certifi.where() self.tls_params = exclude_undefined_keys( { - 'tls': instance.get('tls'), + 'tls': tls, 'tlsCertificateKeyFile': instance.get('tls_certificate_key_file'), 'tlsCAFile': cacert_cert_dir, 'tlsAllowInvalidHostnames': instance.get('tls_allow_invalid_hostnames'), diff --git a/mongo/tests/test_unit_config.py b/mongo/tests/test_unit_config.py index 63a3d90b54811..1b049ae2b9793 100644 --- a/mongo/tests/test_unit_config.py +++ b/mongo/tests/test_unit_config.py @@ -72,6 +72,32 @@ def test_default_tls_params(): assert config.tls_params == {} +def test_atlas_host_auto_enables_tls(): + instance = {'hosts': ['mycluster-shard-00-00.abc123.mongodb.net']} + config = MongoConfig(instance, mock.Mock(), {}) + assert config.tls_params.get('tls') is True + assert 'tlsCAFile' in config.tls_params # certifi CA set automatically + + +def test_atlas_host_explicit_tls_false_respected(): + # Explicit tls: false must not be overridden by auto-detection + instance = {'hosts': ['mycluster-shard-00-00.abc123.mongodb.net'], 'tls': False} + config = MongoConfig(instance, mock.Mock(), {}) + assert config.tls_params.get('tls') is False + + +def test_non_atlas_host_tls_not_auto_enabled(): + instance = {'hosts': ['self-hosted.internal:27017']} + config = MongoConfig(instance, mock.Mock(), {}) + assert config.tls_params == {} + + +def test_atlas_server_uri_auto_enables_tls(): + instance = {'server': 'mongodb://user:pass@mycluster-shard-00-00.abc123.mongodb.net:27017/admin'} + config = MongoConfig(instance, mock.Mock(), {}) + assert config.tls_params.get('tls') is True + + def test_default_scheme(instance): instance['hosts'] = ['test.mongodb.com'] with mock.patch('pymongo.uri_parser.parse_uri', return_value={'nodelist': ["test.mongodb.com"]}) as mock_parse_uri: From f2bf7dfc6303fbb55f918b4019f45c333966b091 Mon Sep 17 00:00:00 2001 From: pierreln-dd Date: Tue, 23 Jun 2026 14:29:06 +0200 Subject: [PATCH 2/3] mongo: add changelog entry for PR 24152 Co-Authored-By: Claude Sonnet 4.6 --- mongo/changelog.d/24152.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 mongo/changelog.d/24152.fixed diff --git a/mongo/changelog.d/24152.fixed b/mongo/changelog.d/24152.fixed new file mode 100644 index 0000000000000..b9914ea081604 --- /dev/null +++ b/mongo/changelog.d/24152.fixed @@ -0,0 +1 @@ +Auto-enable TLS when connecting to MongoDB Atlas hosts to avoid a misleading connection error. From 7bd30f494a1634d91c93e7988bdd94e4ea2cf4e3 Mon Sep 17 00:00:00 2001 From: pierreln-dd Date: Tue, 23 Jun 2026 14:35:26 +0200 Subject: [PATCH 3/3] =?UTF-8?q?mongo:=20fix=20CodeQL=20finding=20=E2=80=94?= =?UTF-8?q?=20anchor=20Atlas=20host=20check=20to=20domain=20suffix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace substring check with suffix check via urlparse to prevent 'evil.mongodb.net.attacker.com' from triggering TLS auto-enable. Add test for the spoofed hostname case. Co-Authored-By: Claude Sonnet 4.6 --- mongo/datadog_checks/mongo/config.py | 27 ++++++++++++++++++++++++++- mongo/tests/test_unit_config.py | 7 +++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/mongo/datadog_checks/mongo/config.py b/mongo/datadog_checks/mongo/config.py index 456cc71843c0b..f2121489f3928 100644 --- a/mongo/datadog_checks/mongo/config.py +++ b/mongo/datadog_checks/mongo/config.py @@ -3,6 +3,7 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import certifi +from urllib.parse import urlparse from datadog_checks.base import ConfigurationError, is_affirmative from datadog_checks.base.utils.common import exclude_undefined_keys @@ -10,6 +11,29 @@ from datadog_checks.mongo.common import DEFAULT_TIMEOUT from datadog_checks.mongo.utils import build_connection_string, parse_mongo_uri +_ATLAS_SUFFIX = '.mongodb.net' + + +def _is_atlas_host(host): + """Return True if the host is a MongoDB Atlas endpoint (ends with .mongodb.net). + + Accepts either a bare host[:port] string or a full MongoDB URI. A substring + check ('mongodb.net' in host) would match attacker-controlled hostnames like + 'evil.mongodb.net.attacker.com', so we anchor to the domain suffix. + """ + host_str = str(host).lower() + if '://' in host_str: + try: + netloc = urlparse(host_str).netloc + if '@' in netloc: + netloc = netloc.split('@', 1)[1] + hostnames = [h.split(':')[0] for h in netloc.split(',')] + except Exception: + return False + else: + hostnames = [host_str.split(':')[0].rstrip('/')] + return any(h == 'mongodb.net' or h.endswith(_ATLAS_SUFFIX) for h in hostnames) + class MongoConfig(object): def __init__(self, instance, log, init_config): @@ -26,7 +50,8 @@ def __init__(self, instance, log, init_config): if isinstance(raw_hosts, str): raw_hosts = [raw_hosts] server = instance.get('server', '') or '' - if any('mongodb.net' in str(h) for h in raw_hosts) or 'mongodb.net' in server: + all_hosts = list(raw_hosts) + ([server] if server else []) + if any(_is_atlas_host(h) for h in all_hosts): tls = True log.debug('Auto-enabling TLS: detected MongoDB Atlas host (mongodb.net)') diff --git a/mongo/tests/test_unit_config.py b/mongo/tests/test_unit_config.py index 1b049ae2b9793..309e969eb9d79 100644 --- a/mongo/tests/test_unit_config.py +++ b/mongo/tests/test_unit_config.py @@ -92,6 +92,13 @@ def test_non_atlas_host_tls_not_auto_enabled(): assert config.tls_params == {} +def test_spoofed_atlas_hostname_not_auto_enabled(): + # 'mongodb.net' appears mid-string — must not trigger auto-TLS + instance = {'hosts': ['evil.mongodb.net.attacker.com:27017']} + config = MongoConfig(instance, mock.Mock(), {}) + assert config.tls_params == {} + + def test_atlas_server_uri_auto_enables_tls(): instance = {'server': 'mongodb://user:pass@mycluster-shard-00-00.abc123.mongodb.net:27017/admin'} config = MongoConfig(instance, mock.Mock(), {})