Merge branch '2016.3' into '2016.11'

Conflicts:
  - conf/master
  - salt/utils/schedule.py
This commit is contained in:
rallytime 2017-06-14 13:24:11 -06:00
commit 89834e49c2
9 changed files with 131 additions and 11 deletions

View File

@ -400,6 +400,20 @@
# will cause minion to throw an exception and drop the message.
# sign_pub_messages: False
# Signature verification on messages published from minions
# This requires that minions cryptographically sign the messages they
# publish to the master. If minions are not signing, then log this information
# at loglevel 'INFO' and drop the message without acting on it.
# require_minion_sign_messages: False
# The below will drop messages when their signatures do not validate.
# Note that when this option is False but `require_minion_sign_messages` is True
# minions MUST sign their messages but the validity of their signatures
# is ignored.
# These two config options exist so a Salt infrastructure can be moved
# to signing minion messages gradually.
# drop_messages_signature_fail: False
# Use TLS/SSL encrypted connection between master and minion.
# Can be set to a dictionary containing keyword arguments corresponding to Python's
# 'ssl.wrap_socket' method.

View File

@ -9,3 +9,18 @@ controls whether a minion can request that the master revoke its key. When True
can request a key revocation and the master will comply. If it is False, the key will not
be revoked by the msater.
New master configuration option `require_minion_sign_messages`
This requires that minions cryptographically sign the messages they
publish to the master. If minions are not signing, then log this information
at loglevel 'INFO' and drop the message without acting on it.
New master configuration option `drop_messages_signature_fail`
Drop messages from minions when their signatures do not validate.
Note that when this option is False but `require_minion_sign_messages` is True
minions MUST sign their messages but the validity of their signatures
is ignored.
New minion configuration option `minion_sign_messages`
Causes the minion to cryptographically sign the payload of messages it places
on the event bus for the master. The payloads are signed with the minion's
private key so the master can verify the signature with its public key.

View File

@ -961,6 +961,19 @@ VALID_OPTS = {
# File chunk size for salt-cp
'salt_cp_chunk_size': int,
# Require that the minion sign messages it posts to the master on the event
# bus
'minion_sign_messages': bool,
# Have master drop messages from minions for which their signatures do
# not verify
'drop_messages_signature_fail': bool,
# Require that payloads from minions have a 'sig' entry
# (in other words, require that minions have 'minion_sign_messages'
# turned on)
'require_minion_sign_messages': bool,
}
# default configurations
@ -1205,6 +1218,7 @@ DEFAULT_MINION_OPTS = {
'ssl': None,
'cache': 'localfs',
'salt_cp_chunk_size': 65536,
'minion_sign_messages': False,
}
DEFAULT_MASTER_OPTS = {
@ -1483,6 +1497,8 @@ DEFAULT_MASTER_OPTS = {
'django_auth_settings': '',
'allow_minion_key_revoke': True,
'salt_cp_chunk_size': 98304,
'require_minion_sign_messages': False,
'drop_messages_signature_fail': False,
}

View File

@ -47,6 +47,7 @@ if not CDOME:
# Import salt libs
import salt.defaults.exitcodes
import salt.utils
import salt.utils.decorators
import salt.payload
import salt.transport.client
import salt.transport.frame
@ -138,13 +139,41 @@ def gen_keys(keydir, keyname, keysize, user=None):
return priv
@salt.utils.decorators.memoize
def _get_key_with_evict(path, timestamp):
'''
Load a key from disk. `timestamp` above is intended to be the timestamp
of the file's last modification. This fn is memoized so if it is called with the
same path and timestamp (the file's last modified time) the second time
the result is returned from the memoiziation. If the file gets modified
then the params are different and the key is loaded from disk.
'''
log.debug('salt.crypt._get_key_with_evict: Loading private key')
with salt.utils.fopen(path) as f:
key = RSA.importKey(f.read())
return key
def _get_rsa_key(path):
'''
Read a key off the disk. Poor man's simple cache in effect here,
we memoize the result of calling _get_rsa_with_evict. This means
the first time _get_key_with_evict is called with a path and a timestamp
the result is cached. If the file (the private key) does not change
then its timestamp will not change and the next time the result is returned
from the cache. If the key DOES change the next time _get_rsa_with_evict
is called it is called with different parameters and the fn is run fully to
retrieve the key from disk.
'''
log.debug('salt.crypt._get_rsa_key: Loading private key')
return _get_key_with_evict(path, os.path.getmtime(path))
def sign_message(privkey_path, message):
'''
Use Crypto.Signature.PKCS1_v1_5 to sign a message. Returns the signature.
'''
log.debug('salt.crypt.sign_message: Loading private key')
with salt.utils.fopen(privkey_path) as f:
key = RSA.importKey(f.read())
key = _get_rsa_key(privkey_path)
log.debug('salt.crypt.sign_message: Signing message.')
signer = PKCS1_v1_5.new(key)
return signer.sign(SHA.new(message))

View File

@ -772,6 +772,7 @@ class RemoteFuncs(object):
# If the return data is invalid, just ignore it
if any(key not in load for key in ('return', 'jid', 'id')):
return False
if load['jid'] == 'req':
# The minion is returning a standalone job, request a jobid
prep_fstr = '{0}.prep_jid'.format(self.opts['master_job_cache'])

View File

@ -18,6 +18,7 @@ import stat
import logging
import multiprocessing
import traceback
import salt.serializers.msgpack
# Import third party libs
try:
@ -1119,8 +1120,10 @@ class AESFuncs(object):
)
)
return False
if 'tok' in load:
load.pop('tok')
return load
def _ext_nodes(self, load):
@ -1403,6 +1406,24 @@ class AESFuncs(object):
:param dict load: The minion payload
'''
if self.opts['require_minion_sign_messages'] and 'sig' not in load:
log.critical('_return: Master is requiring minions to sign their messages, but there is no signature in this payload from {0}.'.format(load['id']))
return False
if 'sig' in load:
log.trace('Verifying signed event publish from minion')
sig = load.pop('sig')
this_minion_pubkey = os.path.join(self.opts['pki_dir'], 'minions/{0}'.format(load['id']))
serialized_load = salt.serializers.msgpack.serialize(load)
if not salt.crypt.verify_signature(this_minion_pubkey, serialized_load, sig):
log.info('Failed to verify event signature from minion {0}.'.format(load['id']))
if self.opts['drop_messages_signature_fail']:
log.critical('Drop_messages_signature_fail is enabled, dropping message from {0}'.format(load['id']))
return False
else:
log.info('But \'drop_message_signature_fail\' is disabled, so message is still accepted.')
load['sig'] = sig
try:
salt.utils.job.store_job(
self.opts, load, event=self.event, mminion=self.mminion)
@ -1446,6 +1467,9 @@ class AESFuncs(object):
ret['fun_args'] = load['arg']
if 'out' in load:
ret['out'] = load['out']
if 'sig' in load:
ret['sig'] = load['sig']
self._return(ret)
def minion_runner(self, clear_load):

View File

@ -20,6 +20,7 @@ import contextlib
import multiprocessing
from random import randint, shuffle
from stat import S_IMODE
import salt.serializers.msgpack
# Import Salt Libs
# pylint: disable=import-error,no-name-in-module,redefined-builtin
@ -1239,11 +1240,25 @@ class Minion(MinionBase):
return functions, returners, errors, executors
def _send_req_sync(self, load, timeout):
if self.opts['minion_sign_messages']:
log.trace('Signing event to be published onto the bus.')
minion_privkey_path = os.path.join(self.opts['pki_dir'], 'minion.pem')
sig = salt.crypt.sign_message(minion_privkey_path, salt.serializers.msgpack.serialize(load))
load['sig'] = sig
channel = salt.transport.Channel.factory(self.opts)
return channel.send(load, timeout=timeout)
@tornado.gen.coroutine
def _send_req_async(self, load, timeout):
if self.opts['minion_sign_messages']:
log.trace('Signing event to be published onto the bus.')
minion_privkey_path = os.path.join(self.opts['pki_dir'], 'minion.pem')
sig = salt.crypt.sign_message(minion_privkey_path, salt.serializers.msgpack.serialize(load))
load['sig'] = sig
channel = salt.transport.client.AsyncReqChannel.factory(self.opts)
ret = yield channel.send(load, timeout=timeout)
raise tornado.gen.Return(ret)

View File

@ -324,6 +324,7 @@ from __future__ import absolute_import, with_statement
import os
import sys
import time
import copy
import signal
import datetime
import itertools
@ -830,7 +831,7 @@ class Schedule(object):
kwargs = {}
if 'kwargs' in data:
kwargs = data['kwargs']
ret['fun_args'].append(data['kwargs'])
ret['fun_args'].append(copy.deepcopy(kwargs))
if func not in self.functions:
ret['return'] = self.functions.missing_fun_string(func)
@ -887,9 +888,9 @@ class Schedule(object):
ret['success'] = False
ret['retcode'] = 254
finally:
try:
# Only attempt to return data to the master
# if the scheduled job is running on a minion.
# Only attempt to return data to the master
# if the scheduled job is running on a minion.
if '__role' in self.opts and self.opts['__role'] == 'minion':
if 'return_job' in data and not data['return_job']:
pass
else:
@ -911,9 +912,13 @@ class Schedule(object):
elif '__role' in self.opts and self.opts['__role'] == 'master':
event = salt.utils.event.get_master_event(self.opts,
self.opts['sock_dir'])
event.fire_event(load, '__schedule_return')
try:
event.fire_event(load, '__schedule_return')
except Exception as exc:
log.exception("Unhandled exception firing event: {0}".format(exc))
log.debug('schedule.handle_func: Removing {0}'.format(proc_fn))
log.debug('schedule.handle_func: Removing {0}'.format(proc_fn))
try:
os.unlink(proc_fn)
except OSError as exc:
if exc.errno == errno.EEXIST or exc.errno == errno.ENOENT:

View File

@ -101,8 +101,9 @@ class CryptTestCase(TestCase):
salt.utils.fopen.assert_has_calls([open_priv_wb, open_pub_wb], any_order=True)
def test_sign_message(self):
with patch('salt.utils.fopen', mock_open(read_data=PRIVKEY_DATA)):
self.assertEqual(SIG, crypt.sign_message('/keydir/keyname.pem', MSG))
key = Crypto.PublicKey.RSA.importKey(PRIVKEY_DATA)
with patch('salt.crypt._get_rsa_key', return_value=key):
self.assertEqual(SIG, salt.crypt.sign_message('/keydir/keyname.pem', MSG))
def test_verify_signature(self):
with patch('salt.utils.fopen', mock_open(read_data=PUBKEY_DATA)):