Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable http_check to function with weak ciphers on SSL/TLS. #1975

Merged
merged 3 commits into from
Oct 19, 2015
Merged
Show file tree
Hide file tree
Changes from 2 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
129 changes: 124 additions & 5 deletions checks.d/http_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,127 @@
import socket
import ssl
import time
import warnings
from urlparse import urlparse

# 3rd party
import requests
import tornado

from requests.adapters import HTTPAdapter
from requests.packages import urllib3
from requests.packages.urllib3.util import ssl_

from requests.packages.urllib3.exceptions import (
SystemTimeWarning,
SecurityWarning,
)
from requests.packages.urllib3.packages.ssl_match_hostname import \
match_hostname

# project
from checks.network_checks import EventType, NetworkCheck, Status
from config import _is_affirmative
from util import headers as agent_headers


class WeakCiphersHTTPSConnection(urllib3.connection.VerifiedHTTPSConnection):

SUPPORTED_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:'
'ECDH+HIGH:DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:'
'RSA+3DES:ECDH+RC4:DH+RC4:RSA+RC4:!aNULL:!eNULL:!EXP:-MD5:RSA+RC4+MD5'
)

def __init__(self, host, port, ciphers=None, **kwargs):
self.ciphers = ciphers if ciphers is not None else self.SUPPORTED_CIPHERS
super(WeakCiphersHTTPSConnection, self).__init__(host, port, **kwargs)

def connect(self):
# Add certificate verification
conn = self._new_conn()

resolved_cert_reqs = ssl_.resolve_cert_reqs(self.cert_reqs)
resolved_ssl_version = ssl_.resolve_ssl_version(self.ssl_version)

hostname = self.host
if getattr(self, '_tunnel_host', None):
# _tunnel_host was added in Python 2.6.3
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can assume that python >= 2.6.3

# (See: # http://hg.python.org/cpython/rev/0f57b30a152f)

self.sock = conn
# Calls self._set_hostport(), so self.host is
# self._tunnel_host below.
self._tunnel()
# Mark this connection as not reusable
self.auto_open = 0

# Override the host with the one we're requesting data from.
hostname = self._tunnel_host

is_time_off = datetime.today().date() < urllib3.connection.RECENT_DATE
if is_time_off:
warnings.warn((
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can assume that the system time is correct. If it's not, there are way more things that would be broken

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually part of the "parent" connect() method in urllib3, I had to overwrite it because I had to change the way we called ssl_wrap_socket and add the ciphers parameter to the call - the rest of the method is pretty much verbatim.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still we can remove it. It pollutes the code for a case that will cause more issues than this ssl connection not working if it happens and that will be caught in other parts of the agent.

'System time is way off (before {0}). This will probably '
'lead to SSL verification errors').format(urllib3.connection.RECENT_DATE),
SystemTimeWarning
)

# Wrap socket using verification with the root certs in trusted_root_certs
self.sock = ssl_.ssl_wrap_socket(conn, self.key_file, self.cert_file,
cert_reqs=resolved_cert_reqs,
ca_certs=self.ca_certs,
server_hostname=hostname,
ssl_version=resolved_ssl_version,
ciphers=self.ciphers)

if self.assert_fingerprint:
ssl_.assert_fingerprint(self.sock.getpeercert(binary_form=True), self.assert_fingerprint)
elif resolved_cert_reqs != ssl.CERT_NONE \
and self.assert_hostname is not False:
cert = self.sock.getpeercert()
if not cert.get('subjectAltName', ()):
warnings.warn((
'Certificate has no `subjectAltName`, falling back to check for a `commonName` for now. '
'This feature is being removed by major browsers and deprecated by RFC 2818. '
'(See https://github.com/shazow/urllib3/issues/497 for details.)'),
SecurityWarning
)
match_hostname(cert, self.assert_hostname or hostname)

self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED
or self.assert_fingerprint is not None)


class WeakCiphersHTTPSConnectionPool(urllib3.connectionpool.HTTPSConnectionPool):

ConnectionCls = WeakCiphersHTTPSConnection


class WeakCiphersPoolManager(urllib3.poolmanager.PoolManager):

def _new_pool(self, scheme, host, port):
if scheme == 'https':
return WeakCiphersHTTPSConnectionPool(host, port, **(self.connection_pool_kw))
return super(WeakCiphersPoolManager, self)._new_pool(scheme, host, port)


class WeakCiphersAdapter(HTTPAdapter):
""""Transport adapter" that allows us to use TLS_RSA_WITH_RC4_128_MD5."""

def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
# Rewrite of the
# requests.adapters.HTTPAdapter.init_poolmanager method
# to use WeakCiphersPoolManager instead of
# urllib3's PoolManager
self._pool_connections = connections
self._pool_maxsize = maxsize
self._pool_block = block

self.poolmanager = WeakCiphersPoolManager(num_pools=connections,
maxsize=maxsize, block=block, strict=True, **pool_kwargs)


def get_ca_certs_path():
"""
Get a path to the trusted certificates of the system
Expand Down Expand Up @@ -61,28 +170,38 @@ def _load_conf(self, instance):
ssl = _is_affirmative(instance.get('disable_ssl_validation', True))
ssl_expire = _is_affirmative(instance.get('check_certificate_expiration', True))
instance_ca_certs = instance.get('ca_certs', self.ca_certs)
weakcipher = _is_affirmative(instance.get('weakciphers', False))

return url, username, password, http_response_status_code, timeout, include_content,\
headers, response_time, content_match, tags, ssl, ssl_expire, instance_ca_certs
headers, response_time, content_match, tags, ssl, ssl_expire, instance_ca_certs,\
weakcipher

def _check(self, instance):
addr, username, password, http_response_status_code, timeout, include_content, headers,\
response_time, content_match, tags, disable_ssl_validation,\
ssl_expire, instance_ca_certs = self._load_conf(instance)
ssl_expire, instance_ca_certs, weakcipher = self._load_conf(instance)
start = time.time()

service_checks = []
try:
parsed_uri = urlparse(addr)
self.log.debug("Connecting to %s" % addr)
if disable_ssl_validation and urlparse(addr)[0] == "https":
if disable_ssl_validation and parsed_uri.scheme == "https":
self.warning("Skipping SSL certificate validation for %s based on configuration"
% addr)

auth = None
if username is not None and password is not None:
auth = (username, password)

r = requests.get(addr, auth=auth, timeout=timeout, headers=headers,
sess = requests.Session()
if weakcipher:
base_addr = '{uri.scheme}://{uri.netloc}/'.format(uri=parsed_uri)
sess.mount(base_addr, WeakCiphersAdapter())
self.log.debug("Weak Ciphers will be used for {0}. Suppoted Cipherlist: {1}".format(
base_addr, WeakCiphersHTTPSConnection.SUPPORTED_CIPHERS))

r = sess.request('GET', addr, auth=auth, timeout=timeout, headers=headers,
verify=False if disable_ssl_validation else instance_ca_certs)

except (socket.timeout, requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
Expand Down Expand Up @@ -156,7 +275,7 @@ def _check(self, instance):
self.SC_STATUS, Status.UP, "UP"
))

if ssl_expire and urlparse(addr)[0] == "https":
if ssl_expire and parsed_uri.scheme == "https":
status, msg = self.check_cert_expiration(instance, timeout, instance_ca_certs)
service_checks.append((
self.SC_SSL_CERT, status, msg
Expand Down
2 changes: 1 addition & 1 deletion tests/checks/integration/test_http_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_http_headers(self):
url, username, password, http_response_status_code, timeout,\
include_content, headers, response_time, content_match,\
tags, ssl, ssl_expiration,\
instance_ca_certs = self.check._load_conf(CONFIG_HTTP_HEADERS['instances'][0])
instance_ca_certs, weakciphers = self.check._load_conf(CONFIG_HTTP_HEADERS['instances'][0])

self.assertEqual(headers["X-Auth-Token"], "SOME-AUTH-TOKEN", headers)
expected_headers = agent_headers(AGENT_CONFIG).get('User-Agent')
Expand Down