mirror of
https://github.com/valitydev/salt.git
synced 2024-11-06 08:35:21 +00:00
Merge branch '2016.3' into '2016.11'
Conflicts: - conf/master - salt/utils/schedule.py
This commit is contained in:
commit
89834e49c2
14
conf/master
14
conf/master
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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'])
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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)):
|
||||
|
Loading…
Reference in New Issue
Block a user