From 20213e0061a279b0cae5ee646f1d56a1d985dd63 Mon Sep 17 00:00:00 2001 From: Felipe Monteiro Date: Thu, 16 Feb 2017 10:49:34 -0500 Subject: [PATCH] Adds TLS/SSL Version Support to Murano Agent. Already implemented for Murano, but need Murano Agent support as well. Currently, Murano supports enabling SSL in Murano Engine [0], but does not have a param for the user to specify the desired SSL version. This is important because some versions of SSL are less secure than others [1]. This patch adds ssl_version support to Murano Agent. [0] https://docs.openstack.org/developer/murano/administrator-guide/deploy_murano/configure_ssl.html [1] https://www.wolfssl.com/wolfSSL/Blog/Entries/2010/10/7_Differences_between_SSL_and_TLS_Protocol_Versions.html Change-Id: I410a38d3c8294ce63c15ae98f627fa5c69a3d118 Partially-Implements: blueprint add-tls-support Depends-On: I71c36c455cde658f402a19c59d7966cee8544cf1 --- muranoagent/app.py | 1 + muranoagent/common/config.py | 6 +++ muranoagent/common/messaging/mqclient.py | 11 +++- muranoagent/tests/unit/test_app.py | 67 +++++++++++++++++++++++- 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/muranoagent/app.py b/muranoagent/app.py index e0f97b0c..c6bad296 100644 --- a/muranoagent/app.py +++ b/muranoagent/app.py @@ -118,6 +118,7 @@ class MuranoAgent(service.Service): 'port': rabbitmq.port, 'virtual_host': rabbitmq.virtual_host, 'ssl': rabbitmq.ssl, + 'ssl_version': rabbitmq.ssl_version, 'ca_certs': rabbitmq.ca_certs.strip() or None, 'insecure': rabbitmq.insecure } diff --git a/muranoagent/common/config.py b/muranoagent/common/config.py index 40f377d4..8ec2df53 100644 --- a/muranoagent/common/config.py +++ b/muranoagent/common/config.py @@ -56,6 +56,12 @@ rabbit_opts = [ help='Boolean flag to enable SSL communication through the ' 'RabbitMQ broker between murano-engine and guest agents.', default=False), + cfg.StrOpt('ssl_version', + default='', + help='SSL version to use (valid only if SSL enabled). ' + 'Valid values are TLSv1 and SSLv23. SSLv2, SSLv3, ' + 'TLSv1_1, and TLSv1_2 may be available on some ' + 'distributions.'), cfg.StrOpt('ca_certs', help='SSL cert file (valid only if SSL enabled).', default=''), diff --git a/muranoagent/common/messaging/mqclient.py b/muranoagent/common/messaging/mqclient.py index 6490699a..ee188b3f 100644 --- a/muranoagent/common/messaging/mqclient.py +++ b/muranoagent/common/messaging/mqclient.py @@ -19,13 +19,14 @@ import ssl as ssl_module import anyjson import eventlet import kombu +from oslo_service import sslutils from muranoagent.common.messaging import subscription class MqClient(object): def __init__(self, login, password, host, port, virtual_host, - ssl=False, ca_certs=None, insecure=False): + ssl=False, ssl_version=None, ca_certs=None, insecure=False): ssl_params = None if ssl: @@ -35,11 +36,19 @@ class MqClient(object): cert_reqs = ssl_module.CERT_OPTIONAL else: cert_reqs = ssl_module.CERT_NONE + ssl_params = { 'ca_certs': ca_certs, 'cert_reqs': cert_reqs } + if ssl_version: + key = ssl_version.lower() + try: + ssl_params['ssl_version'] = sslutils._SSL_PROTOCOLS[key] + except KeyError: + raise RuntimeError("Invalid SSL version: %s" % ssl_version) + # Time interval after which RabbitMQ will disconnect client if no # heartbeats were received. Usually client sends 2 heartbeats during # this interval. Using random to make it less lucky that many agents diff --git a/muranoagent/tests/unit/test_app.py b/muranoagent/tests/unit/test_app.py index ee1ba5f3..3568bb24 100644 --- a/muranoagent/tests/unit/test_app.py +++ b/muranoagent/tests/unit/test_app.py @@ -14,10 +14,14 @@ import fixtures import mock +import ssl as ssl_module + +from oslo_service import sslutils from muranoagent import app from muranoagent import bunch from muranoagent.common import config as cfg +from muranoagent.common.messaging import mqclient from muranoagent import exceptions as exc from muranoagent.tests.unit import base from muranoagent.tests.unit import execution_plan as ep @@ -32,7 +36,8 @@ class TestApp(base.MuranoAgentTestCase, fixtures.FunctionFixture): super(TestApp, self).setUp() mock_path.return_value = True self.agent = app.MuranoAgent() - CONF.set_override('storage', 'cache') + CONF.set_override('storage', 'cache', enforce_type=True) + self.addCleanup(CONF.clear_override, 'storage') def test_verify_execution_plan_downloable(self): template = self.useFixture(ep.ExPlanDownloable()).execution_plan @@ -80,3 +85,63 @@ class TestApp(base.MuranoAgentTestCase, fixtures.FunctionFixture): template = self.useFixture(ep.ExPlanBerkWrongVersion()).execution_plan self.assertRaises(exc.IncorrectFormat, self.agent._verify_plan, template) + + @mock.patch.object(mqclient, 'random', autospec=True) + @mock.patch.object(mqclient, 'kombu', autospec=True) + def test_rmq_client_initialization_with_ssl_version(self, mock_kombu, + mock_random): + expected_heartbeat = 20 # 20 = 20 + 20 * 0, due to mocked value below. + mock_random.random.return_value = 0 + + ssl_versions = ( + ('tlsv1', getattr(ssl_module, 'PROTOCOL_TLSv1', None)), + ('tlsv1_1', getattr(ssl_module, 'PROTOCOL_TLSv1_1', None)), + ('tlsv1_2', getattr(ssl_module, 'PROTOCOL_TLSv1_2', None)), + ('sslv2', getattr(ssl_module, 'PROTOCOL_SSLv2', None)), + ('sslv23', getattr(ssl_module, 'PROTOCOL_SSLv23', None)), + ('sslv3', getattr(ssl_module, 'PROTOCOL_SSLv3', None))) + exception_count = 0 + + for ssl_name, ssl_version in ssl_versions: + ssl_kwargs = { + 'login': 'test_login', + 'password': 'test_password', + 'host': 'test_host', + 'port': 'test_port', + 'virtual_host': 'test_virtual_host', + 'ssl': True, + 'ssl_version': ssl_name, + 'ca_certs': ['cert1'], + 'insecure': False + } + + # If a ssl_version is not valid, a RuntimeError is thrown. + # According to the ssl_version docs in config.py, certain versions + # of TLS may be available depending on the system. So, just + # check that at least 1 ssl_version works. + if ssl_version is None: + e = self.assertRaises(RuntimeError, mqclient.MqClient, + **ssl_kwargs) + self.assertEqual('Invalid SSL version: %s' % ssl_name, + e.__str__()) + exception_count += 1 + continue + + self.ssl_client = mqclient.MqClient(**ssl_kwargs) + + mock_kombu.Connection.assert_called_once_with( + 'amqp://{0}:{1}@{2}:{3}/{4}'.format( + 'test_login', 'test_password', 'test_host', 'test_port', + 'test_virtual_host'), + heartbeat=expected_heartbeat, + ssl={'ca_certs': ['cert1'], + 'cert_reqs': ssl_module.CERT_REQUIRED, + 'ssl_version': ssl_version}) + self.assertEqual( + mock_kombu.Connection(), self.ssl_client._connection) + self.assertIsNone(self.ssl_client._channel) + self.assertFalse(self.ssl_client._connected) + mock_kombu.Connection.reset_mock() + + # Check that at least one ssl_version worked. + self.assertGreater(len(ssl_versions), exception_count)