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. diff --git a/mongo/datadog_checks/mongo/config.py b/mongo/datadog_checks/mongo/config.py index a2b244ef259df..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): @@ -18,15 +42,28 @@ 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 '' + 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)') + 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..309e969eb9d79 100644 --- a/mongo/tests/test_unit_config.py +++ b/mongo/tests/test_unit_config.py @@ -72,6 +72,39 @@ 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_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(), {}) + 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: