Skip to content

Commit 99292e5

Browse files
committed
Add core TLS context changes
1 parent 88af1c4 commit 99292e5

File tree

3 files changed

+92
-54
lines changed

3 files changed

+92
-54
lines changed

datadog_checks_base/datadog_checks/base/utils/http.py

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222

2323
from datadog_checks.base.agent import datadog_agent
2424
from datadog_checks.base.utils import _http_utils
25+
from datadog_checks.base.utils.tls import TlsContextWrapper
2526

2627
from ..config import is_affirmative
2728
from ..errors import ConfigurationError
2829
from .common import ensure_bytes, ensure_unicode
2930
from .headers import get_default_headers, update_headers
30-
from .network import CertAdapter, create_socket_connection
31+
from .network import create_socket_connection
3132
from .time import get_timestamp
3233

3334
# See Performance Optimizations in this package's README.md.
@@ -85,12 +86,14 @@
8586
'skip_proxy': False,
8687
'tls_ca_cert': None,
8788
'tls_cert': None,
89+
'tls_ciphers': 'ALL',
8890
'tls_use_host_header': False,
8991
'tls_ignore_warning': False,
9092
'tls_private_key': None,
93+
'tls_private_key_password': None,
9194
'tls_protocols_allowed': DEFAULT_PROTOCOL_VERSIONS,
95+
'tls_validate_hostname': True,
9296
'tls_verify': True,
93-
'tls_ciphers': 'ALL',
9497
'timeout': DEFAULT_TIMEOUT,
9598
'use_legacy_auth_encoding': True,
9699
'username': None,
@@ -115,6 +118,22 @@
115118
UDS_SCHEME = 'unix'
116119

117120

121+
class HTTPAdapterWrapper(requests.adapters.HTTPAdapter):
122+
"""
123+
HTTPS adapter that uses TlsContextWrapper to create SSL contexts.
124+
This ensures consistent TLS configuration across all HTTPS requests.
125+
"""
126+
127+
def __init__(self, tls_context_wrapper, **kwargs):
128+
self.tls_context_wrapper = tls_context_wrapper
129+
super(HTTPAdapterWrapper, self).__init__()
130+
131+
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
132+
# Use the TLS context from TlsContextWrapper
133+
pool_kwargs['ssl_context'] = self.tls_context_wrapper.tls_context
134+
return super(HTTPAdapterWrapper, self).init_poolmanager(connections, maxsize, block=block, **pool_kwargs)
135+
136+
118137
class ResponseWrapper(ObjectProxy):
119138
def __init__(self, response, default_chunk_size):
120139
super(ResponseWrapper, self).__init__(response)
@@ -152,7 +171,7 @@ class RequestsWrapper(object):
152171
'auth_token_handler',
153172
'request_size',
154173
'tls_protocols_allowed',
155-
'tls_ciphers_allowed',
174+
'tls_context_wrapper',
156175
)
157176

158177
def __init__(self, instance, init_config, remapper=None, logger=None, session=None):
@@ -254,7 +273,8 @@ def __init__(self, instance, init_config, remapper=None, logger=None, session=No
254273

255274
allow_redirects = is_affirmative(config['allow_redirects'])
256275

257-
# https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification
276+
# For TLS verification, we now rely on the TLS context wrapper
277+
# but still need to set verify for requests compatibility
258278
verify = True
259279
if isinstance(config['tls_ca_cert'], str):
260280
verify = config['tls_ca_cert']
@@ -347,13 +367,8 @@ def __init__(self, instance, init_config, remapper=None, logger=None, session=No
347367
if config['kerberos_cache']:
348368
self.request_hooks.append(lambda: handle_kerberos_cache(config['kerberos_cache']))
349369

350-
ciphers = config.get('tls_ciphers')
351-
if ciphers:
352-
if 'ALL' in ciphers:
353-
updated_ciphers = "ALL"
354-
else:
355-
updated_ciphers = ":".join(ciphers)
356-
self.tls_ciphers_allowed = updated_ciphers
370+
# Create TLS context wrapper for consistent TLS configuration
371+
self.tls_context_wrapper = TlsContextWrapper(config, remapper)
357372

358373
def get(self, url, **options):
359374
return self._request('get', url, options)
@@ -397,6 +412,11 @@ def _request(self, method, url, options):
397412
new_options['headers'] = new_options['headers'].copy()
398413
new_options['headers'].update(extra_headers)
399414

415+
if new_options['verify'] != self.tls_context_wrapper.config['tls_verify']:
416+
# The verify option needs to be synchronized
417+
self.tls_context_wrapper.config['tls_verify'] = new_options['verify']
418+
self.tls_context_wrapper.refresh_tls_context()
419+
400420
if is_uds_url(url):
401421
persist = True # UDS support is only enabled on the shared session.
402422
url = quote_uds_url(url)
@@ -409,7 +429,8 @@ def _request(self, method, url, options):
409429
if persist:
410430
request_method = getattr(self.session, method)
411431
else:
412-
request_method = getattr(requests, method)
432+
# Create a new session for non-persistent requests
433+
request_method = getattr(self.create_session(), method)
413434

414435
if self.auth_token_handler:
415436
try:
@@ -435,16 +456,18 @@ def make_request_aia_chasing(self, request_method, method, url, new_options, per
435456
certs = self.fetch_intermediate_certs(hostname, port)
436457
if not certs:
437458
raise e
459+
self.tls_context_wrapper.config['tls_ca_cert'] = certs
460+
self.tls_context_wrapper.refresh_tls_context()
438461
# retry the connection via session object
439-
certadapter = CertAdapter(certs=certs)
440462
if not persist:
441463
session = requests.Session()
442464
for option, value in self.options.items():
443465
setattr(session, option, value)
466+
certadapter = HTTPAdapterWrapper(self.tls_context_wrapper)
467+
session.mount(url, certadapter)
444468
else:
445469
session = self.session
446470
request_method = getattr(session, method)
447-
session.mount(url, certadapter)
448471
response = request_method(url, **new_options)
449472
return response
450473

@@ -472,8 +495,17 @@ def fetch_intermediate_certs(self, hostname, port=443):
472495
with sock:
473496
try:
474497
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
498+
# Override verify mode for intermediate cert discovery
475499
context.verify_mode = ssl.CERT_NONE
476-
context.set_ciphers(self.tls_ciphers_allowed)
500+
# Set the ciphers
501+
ciphers = self.tls_context_wrapper.config.get('tls_ciphers')
502+
if ciphers:
503+
if 'ALL' in ciphers:
504+
updated_ciphers = "ALL"
505+
else:
506+
updated_ciphers = ":".join(ciphers)
507+
508+
context.set_ciphers(updated_ciphers)
477509

478510
with context.wrap_socket(sock, server_hostname=hostname) as secure_sock:
479511
der_cert = secure_sock.getpeercert(binary_form=True)
@@ -521,7 +553,7 @@ def load_intermediate_certs(self, der_cert, certs):
521553

522554
# Assume HTTP for now
523555
try:
524-
response = requests.get(uri) # SKIP_HTTP_VALIDATION
556+
response = self.get(uri, verify=False) # SKIP_HTTP_VALIDATION
525557
except Exception as e:
526558
self.logger.error('Error fetching intermediate certificate from `%s`: %s', uri, e)
527559
continue
@@ -532,23 +564,46 @@ def load_intermediate_certs(self, der_cert, certs):
532564
self.load_intermediate_certs(intermediate_cert, certs)
533565
return certs
534566

535-
@property
536-
def session(self):
537-
if self._session is None:
538-
self._session = requests.Session()
567+
def create_session(self):
568+
session = requests.Session()
569+
570+
# Use TlsContextHTTPSAdapter for consistent TLS configuration
571+
https_adapter = HTTPAdapterWrapper(self.tls_context_wrapper)
572+
573+
# Enables HostHeaderSSLAdapter if needed
574+
# https://toolbelt.readthedocs.io/en/latest/adapters.html#hostheaderssladapter
575+
if self.tls_use_host_header:
576+
# Create a combined adapter that supports both TLS context and host headers
577+
class TlsContextHostHeaderAdapter(HTTPAdapterWrapper, _http_utils.HostHeaderSSLAdapter):
578+
def __init__(self, tls_context_wrapper, **kwargs):
579+
HTTPAdapterWrapper.__init__(self, tls_context_wrapper, **kwargs)
580+
_http_utils.HostHeaderSSLAdapter.__init__(self, **kwargs)
581+
582+
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
583+
# Use TLS context from wrapper
584+
pool_kwargs['ssl_context'] = self.tls_context_wrapper.tls_context
585+
return _http_utils.HostHeaderSSLAdapter.init_poolmanager(
586+
self, connections, maxsize, block=block, **pool_kwargs
587+
)
539588

540-
# Enables HostHeaderSSLAdapter
541-
# https://toolbelt.readthedocs.io/en/latest/adapters.html#hostheaderssladapter
542-
if self.tls_use_host_header:
543-
self._session.mount('https://', _http_utils.HostHeaderSSLAdapter())
544-
# Enable Unix Domain Socket (UDS) support.
545-
# See: https://github.com/msabramo/requests-unixsocket
546-
self._session.mount('{}://'.format(UDS_SCHEME), requests_unixsocket.UnixAdapter())
589+
https_adapter = TlsContextHostHeaderAdapter(self.tls_context_wrapper)
547590

548-
# Attributes can't be passed to the constructor
549-
for option, value in self.options.items():
550-
setattr(self._session, option, value)
591+
session.mount('https://', https_adapter)
551592

593+
# Enable Unix Domain Socket (UDS) support.
594+
# See: https://github.com/msabramo/requests-unixsocket
595+
session.mount('{}://'.format(UDS_SCHEME), requests_unixsocket.UnixAdapter())
596+
597+
# Attributes can't be passed to the constructor
598+
for option, value in self.options.items():
599+
setattr(session, option, value)
600+
return session
601+
602+
@property
603+
def session(self):
604+
if self._session is None:
605+
# Create a new session if it doesn't exist
606+
self._session = self.create_session()
552607
return self._session
553608

554609
def handle_auth_token(self, **request):

datadog_checks_base/datadog_checks/base/utils/network.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import socket
2-
import ssl
3-
4-
from requests.adapters import HTTPAdapter, PoolManager
52

63

74
def create_socket_connection(hostname, port=443, sock_type=socket.SOCK_STREAM, timeout=10):
@@ -34,16 +31,3 @@ def create_socket_connection(hostname, port=443, sock_type=socket.SOCK_STREAM, t
3431
raise socket.error('Unable to resolve host, check your DNS: {}'.format(message)) # noqa: G
3532

3633
raise
37-
38-
39-
class CertAdapter(HTTPAdapter):
40-
def __init__(self, **kwargs):
41-
self.certs = kwargs['certs']
42-
super(CertAdapter, self).__init__()
43-
44-
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
45-
context = ssl.create_default_context()
46-
for cert in self.certs:
47-
context.load_verify_locations(cadata=cert)
48-
pool_kwargs['ssl_context'] = context
49-
self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize, block=block, strict=True, **pool_kwargs)

datadog_checks_base/datadog_checks/base/utils/tls.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,18 +104,17 @@ def _create_tls_context(self):
104104
# type: () -> ssl.SSLContext
105105

106106
# https://docs.python.org/3/library/ssl.html#ssl.SSLContext
107-
# https://docs.python.org/3/library/ssl.html#ssl.PROTOCOL_TLS
108-
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
107+
# https://docs.python.org/3/library/ssl.html#ssl.PROTOCOL_TLS_CLIENT
108+
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
109+
110+
# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.check_hostname
111+
context.check_hostname = (
112+
False if not self.config['tls_verify'] else self.config.get('tls_validate_hostname', True)
113+
)
109114

110115
# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.verify_mode
111116
context.verify_mode = ssl.CERT_REQUIRED if self.config['tls_verify'] else ssl.CERT_NONE
112117

113-
# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.check_hostname
114-
if context.verify_mode == ssl.CERT_REQUIRED:
115-
context.check_hostname = self.config.get('tls_validate_hostname', True)
116-
else:
117-
context.check_hostname = False
118-
119118
ciphers = self.config.get('tls_ciphers')
120119
if ciphers:
121120
if 'ALL' in ciphers:

0 commit comments

Comments
 (0)