Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mongo/changelog.d/24152.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Auto-enable TLS when connecting to MongoDB Atlas hosts to avoid a misleading connection error.
41 changes: 39 additions & 2 deletions mongo/datadog_checks/mongo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,37 @@
# 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
from datadog_checks.base.utils.db.utils import get_agent_host_tags
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):
Expand All @@ -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'),
Expand Down
33 changes: 33 additions & 0 deletions mongo/tests/test_unit_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading