Merge "Murano-engine side implementation of agent message signing"

This commit is contained in:
Zuul 2018-01-17 02:32:27 +00:00 committed by Gerrit Code Review
commit c1613c2bc8
11 changed files with 115 additions and 13 deletions

View File

@ -79,6 +79,7 @@ Methods:
"%RABBITMQ_INSECURE%": str($rabbitMqParams.insecure).toLower()
"%RABBITMQ_INPUT_QUEUE%": $.agent.queueName()
"%RESULT_QUEUE%": $region.agentListener.queueName()
"%SIGNING_KEY%": $.agent.signingKey('\t')
- $scriptReplacements:
"%AGENT_CONFIG_BASE64%": base64encode($configFile.replace($configReplacements))
"%INTERNAL_HOSTNAME%": $.name

View File

@ -50,6 +50,7 @@ Methods:
"%RABBITMQ_SSL%": str($rabbitMqParams.ssl).toLower()
"%RABBITMQ_INPUT_QUEUE%": $.agent.queueName()
"%RESULT_QUEUE%": $region.agentListener.queueName()
"%SIGNING_KEY%": $.agent.signingKey('')
- $scriptReplacements:
"%AGENT_CONFIG_BASE64%": base64encode($configFile.replace($configReplacements))
"%INTERNAL_HOSTNAME%": $.name

View File

@ -4,7 +4,7 @@
<section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/>
</startup>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
@ -32,5 +32,7 @@
<add key="rabbitmq.allowInvalidCA" value="true"/>
<add key="rabbitmq.sslServerName" value=""/>
<add key="engine.key" value="%SIGNING_KEY%"/>
</appSettings>
</configuration>

View File

@ -5,6 +5,9 @@ log_file = /var/log/murano-agent.log
storage=/var/murano/plans
engine_key =
%SIGNING_KEY%
[rabbitmq]
# Input queue name

View File

@ -220,6 +220,9 @@ engine_opts = [
'for legacy behavior using murano-api) or glance '
'(stands for glance-glare artifact service)'),
deprecated_group='packages_opts'),
cfg.StrOpt('signing_key', default='~/.ssh/id_rsa',
help=_('Path to RSA key for agent message signing')),
]
# TODO(sjmc7): move into engine opts?

View File

@ -102,16 +102,22 @@ class MqClient(object):
bound_queue = queue(self._connection)
bound_queue.declare()
def send(self, message, key, exchange=''):
def send(self, message, key, exchange='', signing_func=None):
if not self._connected:
raise RuntimeError('Not connected to RabbitMQ')
producer = kombu.Producer(self._connection)
data = jsonutils.dumps(message.body)
headers = None
if signing_func:
headers = {'signature': signing_func(data)}
producer.publish(
exchange=str(exchange),
routing_key=str(key),
body=jsonutils.dumps(message.body),
message_id=str(message.id)
body=data,
message_id=str(message.id),
headers=headers
)
def open(self, queue, prefetch_count=1):

View File

@ -16,8 +16,14 @@
import copy
import datetime
import os
import os.path
import time
import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
import eventlet.event
from oslo_config import cfg
from oslo_log import log as logging
@ -49,12 +55,33 @@ class Agent(object):
self._enabled = not CONF.engine.disable_murano_agent
env = host.find_owner('io.murano.Environment')
self._queue = str('e%s-h%s' % (env.id, host.id)).lower()
self._signing_key = None
if CONF.engine.signing_key:
key_path = os.path.expanduser(CONF.engine.signing_key)
if not os.path.exists(key_path):
LOG.warn("Key file %s does not exist. "
"Message signing is disabled")
else:
with open(key_path, "rb") as key_file:
key_data = key_file.read()
self._signing_key = serialization.load_pem_private_key(
key_data, password=None, backend=default_backend())
self._last_stamp = 0
self._initialized = False
@property
def enabled(self):
return self._enabled
@specs.parameter('line_prefix', specs.yaqltypes.String())
def signing_key(self, line_prefix=''):
if not self._signing_key:
return ""
key = self._signing_key.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.PKCS1)
return line_prefix + line_prefix.join(key.splitlines(True))
def _initialize(self):
if self._initialized:
return
@ -96,8 +123,7 @@ class Agent(object):
msg = self._prepare_message(template, msg_id)
with common.create_rmq_client(region) as client:
client.send(message=msg, key=self._queue)
client.send(message=msg, key=self._queue, signing_func=self._sign)
if wait_results:
try:
with eventlet.Timeout(timeout):
@ -160,6 +186,13 @@ class Agent(object):
template = {'Body': 'return', 'FormatVersion': '2.0.0', 'Scripts': {}}
self.call_raw(template, timeout)
def _sign(self, msg):
if not self._signing_key:
return None
return self._signing_key.sign(
(self._queue + msg).encode('utf-8'),
padding.PKCS1v15(), hashes.SHA256())
def _process_v1_result(self, result):
if result['IsException']:
raise AgentException(dict(self._get_exception_info(
@ -216,6 +249,13 @@ class Agent(object):
else:
return self._build_v2_execution_plan(template, resources)
def _generate_stamp(self):
stamp = int(time.time() * 10000)
if stamp <= self._last_stamp:
stamp = self._last_stamp + 1
self._last_stamp = stamp
return stamp
def _build_v1_execution_plan(self, template, resources):
scripts_folder = 'scripts'
script_files = template.get('Scripts', [])
@ -226,12 +266,15 @@ class Agent(object):
resources.string(script_path, binary=True),
encoding='latin1'))
template['Scripts'] = scripts
template['Stamp'] = self._generate_stamp()
return template
def _build_v2_execution_plan(self, template, resources):
scripts_folder = 'scripts'
plan_id = uuid.uuid4().hex
template['ID'] = plan_id
template['Stamp'] = self._generate_stamp()
if 'Action' not in template:
template['Action'] = 'Execute'
if 'Files' not in template:

View File

@ -209,7 +209,23 @@ class MQClientTest(base.MuranoTestCase):
self.ssl_client._connection)
mock_kombu.Producer().publish.assert_called_once_with(
exchange='test_exchange', routing_key='test_key',
body=jsonutils.dumps('test_message'), message_id='3')
body=jsonutils.dumps('test_message'), message_id='3',
headers=None)
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_send_signed(self, mock_kombu):
mock_message = mock.MagicMock(body='test_message', id=3)
signer = lambda msg: "SIGNATURE"
self.ssl_client.connect()
self.ssl_client.send(mock_message, 'test_key', 'test_exchange', signer)
mock_kombu.Producer.assert_called_once_with(
self.ssl_client._connection)
mock_kombu.Producer().publish.assert_called_once_with(
exchange='test_exchange', routing_key='test_key',
body=jsonutils.dumps('test_message'), message_id='3',
headers={'signature': 'SIGNATURE'})
def test_send_except_runtime_error(self):
with self.assertRaisesRegex(RuntimeError,

View File

@ -94,13 +94,15 @@ class TestAgent(base.MuranoTestCase):
self.assertRaises(exceptions.PolicyViolationException,
self.agent.send_raw, {})
@mock.patch('murano.engine.system.agent.Agent._sign')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_send(self, mock_kombu):
def test_send(self, mock_kombu, mock_sign):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
self.agent._queue = 'test_queue'
mock_sign.return_value = 'SIGNATURE'
plan = self.agent.build_execution_plan(template, self.resources)
with mock.patch.object(self.agent, 'build_execution_plan',
return_value=plan):
@ -111,7 +113,9 @@ class TestAgent(base.MuranoTestCase):
exchange='',
routing_key='test_queue',
body=json.dumps(plan),
message_id=plan['ID'])
message_id=plan['ID'],
headers={'signature': 'SIGNATURE'}
)
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
@ -129,9 +133,10 @@ class TestAgent(base.MuranoTestCase):
self.assertFalse(self.agent.is_ready(1))
@mock.patch('murano.engine.system.agent.Agent._sign')
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_call_with_v1_result(self, mock_kombu, mock_event):
def test_call_with_v1_result(self, mock_kombu, mock_event, mock_sign):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
@ -149,6 +154,7 @@ class TestAgent(base.MuranoTestCase):
mock_event().wait.side_effect = None
mock_event().wait.return_value = test_v1_result
mock_sign.return_value = 'SIGNATURE'
self.agent._queue = 'test_queue'
plan = self.agent.build_execution_plan(template, self.resources)
@ -164,11 +170,14 @@ class TestAgent(base.MuranoTestCase):
exchange='',
routing_key='test_queue',
body=json.dumps(plan),
message_id=plan['ID'])
message_id=plan['ID'],
headers={'signature': 'SIGNATURE'}
)
@mock.patch('murano.engine.system.agent.Agent._sign')
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
def test_call_with_v2_result(self, mock_kombu, mock_event):
def test_call_with_v2_result(self, mock_kombu, mock_event, mock_sign):
template = yamllib.load(
self._read('template_with_files.template'),
Loader=self.yaml_loader)
@ -179,6 +188,7 @@ class TestAgent(base.MuranoTestCase):
mock_event().wait.side_effect = None
mock_event().wait.return_value = v2_result
mock_sign.return_value = 'SIGNATURE'
self.agent._queue = 'test_queue'
plan = self.agent.build_execution_plan(template, self.resources)
@ -194,7 +204,9 @@ class TestAgent(base.MuranoTestCase):
exchange='',
routing_key='test_queue',
body=json.dumps(plan),
message_id=plan['ID'])
message_id=plan['ID'],
headers={'signature': 'SIGNATURE'}
)
@mock.patch('murano.engine.system.agent.eventlet.event.Event')
@mock.patch('murano.common.messaging.mqclient.kombu')
@ -357,6 +369,8 @@ class TestExecutionPlan(base.MuranoTestCase):
self.resources.string.return_value = 'text'
self.uuids = ['ID1', 'ID2', 'ID3', 'ID4']
self.mock_uuid = self._stub_uuid(self.uuids)
time_mock = mock.patch('time.time').start()
time_mock.return_value = 2
self.addCleanup(mock.patch.stopall)
def _read(self, path):
@ -430,6 +444,7 @@ class TestExecutionPlan(base.MuranoTestCase):
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Tomcat',
'Parameters': {
'appName': '$appName'
@ -470,6 +485,7 @@ class TestExecutionPlan(base.MuranoTestCase):
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Tomcat',
'Parameters': {
'appName': '$appName'
@ -504,6 +520,7 @@ class TestExecutionPlan(base.MuranoTestCase):
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Tomcat',
'Parameters': {
'appName': '$appName'
@ -542,6 +559,7 @@ class TestExecutionPlan(base.MuranoTestCase):
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Chef',
'Parameters': {
'appName': '$appName'
@ -587,6 +605,7 @@ class TestExecutionPlan(base.MuranoTestCase):
},
'FormatVersion': '2.0.0',
'ID': self.uuids[0],
'Stamp': 20000,
'Name': 'Deploy Telnet',
'Parameters': {
'appName': '$appName'

View File

@ -0,0 +1,7 @@
---
features:
- |
Murano engine can be configured to sign all the RabbitMQ messages sent
to the agents. When the RSA key is provided, engine will provide agents
with its public part and sign all the messages sent. Agents then will
ignore any command that was not sent by the engine.

View File

@ -22,6 +22,7 @@ keystonemiddleware>=4.17.0 # Apache-2.0
testtools>=2.2.0 # MIT
yaql>=1.1.3 # Apache 2.0 License
debtcollector>=1.2.0 # Apache-2.0
cryptography>=1.9,!=2.0 # BSD/Apache-2.0
# For paste.util.template used in keystone.common.template
Paste>=2.0.2 # MIT