mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge pull request #8734 from terminalmage/autoreject_file
add an autoreject_file parameter to the master config file
This commit is contained in:
commit
b3b6c0bcee
4 changed files with 192 additions and 118 deletions
14
conf/master
14
conf/master
|
@ -56,7 +56,8 @@
|
|||
#pidfile: /var/run/salt-master.pid
|
||||
|
||||
# The root directory prepended to these options: pki_dir, cachedir,
|
||||
# sock_dir, log_file, autosign_file, extension_modules, key_logfile, pidfile.
|
||||
# sock_dir, log_file, autosign_file, autoreject_file, extension_modules,
|
||||
# key_logfile, pidfile.
|
||||
#root_dir: /
|
||||
|
||||
# Directory used to store public key data
|
||||
|
@ -134,9 +135,14 @@
|
|||
# public keys from the minions. Note that this is insecure.
|
||||
#auto_accept: False
|
||||
|
||||
# If the autosign_file is specified only incoming keys specified in
|
||||
# the autosign_file will be automatically accepted. This is insecure.
|
||||
# Regular expressions as well as globing lines are supported.
|
||||
# If the autosign_file is specified, incoming keys specified in the
|
||||
# autosign_file will be automatically accepted. This is insecure. Regular
|
||||
# expressions as well as globing lines are supported.
|
||||
#autosign_file: /etc/salt/autosign.conf
|
||||
|
||||
# Works like autosign_file, but instead allows you to specify minion IDs for
|
||||
# which keys will automatically be rejected. Will override both membership in
|
||||
# the autosign_file and the auto_accept setting.
|
||||
#autosign_file: /etc/salt/autosign.conf
|
||||
|
||||
# Enable permissive access to the salt keys. This allows you to run the
|
||||
|
|
|
@ -284,7 +284,7 @@ mode set this value to ``True``.
|
|||
Default: ``False``
|
||||
|
||||
Enable auto_accept. This setting will automatically accept all incoming
|
||||
public keys from the minions
|
||||
public keys from minions.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
@ -295,13 +295,25 @@ public keys from the minions
|
|||
``autosign_file``
|
||||
-----------------
|
||||
|
||||
Default ``not defined``
|
||||
Default: ``not defined``
|
||||
|
||||
If the autosign_file is specified incoming keys specified in the autosign_file
|
||||
will be automatically accepted. Matches will be searched for first by string
|
||||
comparison, then by globbing, then by full-string regex matching. This is
|
||||
If the ``autosign_file`` is specified incoming keys specified in the autosign_file
|
||||
will be automatically accepted. Matches will be searched for first by string
|
||||
comparison, then by globbing, then by full-string regex matching. This is
|
||||
insecure!
|
||||
|
||||
``autoreject_file``
|
||||
-------------------
|
||||
|
||||
.. versionadded:: Hydrogen
|
||||
|
||||
Default: ``not defined``
|
||||
|
||||
Works like :conf_master:`autosign_file`, but instead allows you to specify
|
||||
minion IDs for which keys will automatically be rejected. Will override both
|
||||
membership in the :conf_master:`autosign_file` and the
|
||||
:conf_master:`auto_accept` setting.
|
||||
|
||||
.. conf_master:: client_acl
|
||||
|
||||
``client_acl``
|
||||
|
|
|
@ -730,8 +730,8 @@ def syndic_config(master_config_path,
|
|||
opts.update(syndic_opts)
|
||||
# Prepend root_dir to other paths
|
||||
prepend_root_dirs = [
|
||||
'pki_dir', 'cachedir', 'pidfile', 'sock_dir',
|
||||
'extension_modules', 'autosign_file', 'token_dir'
|
||||
'pki_dir', 'cachedir', 'pidfile', 'sock_dir', 'extension_modules',
|
||||
'autosign_file', 'autoreject_file', 'token_dir'
|
||||
]
|
||||
for config_key in ('log_file', 'key_logfile'):
|
||||
if urlparse.urlparse(opts.get(config_key, '')).scheme == '':
|
||||
|
@ -1874,7 +1874,7 @@ def apply_master_config(overrides=None, defaults=None):
|
|||
# Prepend root_dir to other paths
|
||||
prepend_root_dirs = [
|
||||
'pki_dir', 'cachedir', 'pidfile', 'sock_dir', 'extension_modules',
|
||||
'autosign_file', 'token_dir'
|
||||
'autosign_file', 'autoreject_file', 'token_dir'
|
||||
]
|
||||
|
||||
# These can be set to syslog, so, not actual paths on the system
|
||||
|
|
268
salt/master.py
268
salt/master.py
|
@ -1484,13 +1484,13 @@ class AESFuncs(object):
|
|||
return {}
|
||||
# Set up the publication payload
|
||||
load = {
|
||||
'fun': clear_load['fun'],
|
||||
'arg': clear_load['arg'],
|
||||
'expr_form': clear_load.get('tgt_type', 'glob'),
|
||||
'tgt': clear_load['tgt'],
|
||||
'ret': clear_load['ret'],
|
||||
'id': clear_load['id'],
|
||||
}
|
||||
'fun': clear_load['fun'],
|
||||
'arg': clear_load['arg'],
|
||||
'expr_form': clear_load.get('tgt_type', 'glob'),
|
||||
'tgt': clear_load['tgt'],
|
||||
'ret': clear_load['ret'],
|
||||
'id': clear_load['id'],
|
||||
}
|
||||
if 'tgt_type' in clear_load:
|
||||
if clear_load['tgt_type'].startswith('node'):
|
||||
if clear_load['tgt'] in self.opts['nodegroups']:
|
||||
|
@ -1538,13 +1538,13 @@ class AESFuncs(object):
|
|||
return {}
|
||||
# Set up the publication payload
|
||||
load = {
|
||||
'fun': clear_load['fun'],
|
||||
'arg': clear_load['arg'],
|
||||
'expr_form': clear_load.get('tgt_type', 'glob'),
|
||||
'tgt': clear_load['tgt'],
|
||||
'ret': clear_load['ret'],
|
||||
'id': clear_load['id'],
|
||||
}
|
||||
'fun': clear_load['fun'],
|
||||
'arg': clear_load['arg'],
|
||||
'expr_form': clear_load.get('tgt_type', 'glob'),
|
||||
'tgt': clear_load['tgt'],
|
||||
'ret': clear_load['ret'],
|
||||
'id': clear_load['id'],
|
||||
}
|
||||
if 'tmo' in clear_load:
|
||||
try:
|
||||
load['timeout'] = int(clear_load['tmo'])
|
||||
|
@ -1782,25 +1782,19 @@ class ClearFuncs(object):
|
|||
|
||||
return False
|
||||
|
||||
def _check_autosign(self, keyid):
|
||||
def _check_signing_file(self, keyid, signing_file):
|
||||
'''
|
||||
Checks if the specified keyid should automatically be signed.
|
||||
Check a keyid for membership in a signing file
|
||||
'''
|
||||
|
||||
if self.opts['auto_accept']:
|
||||
return True
|
||||
|
||||
autosign_file = self.opts.get('autosign_file', None)
|
||||
|
||||
if not autosign_file or not os.path.exists(autosign_file):
|
||||
if not signing_file or not os.path.exists(signing_file):
|
||||
return False
|
||||
|
||||
if not self._check_permissions(autosign_file):
|
||||
if not self._check_permissions(signing_file):
|
||||
message = 'Wrong permissions for {0}, ignoring content'
|
||||
log.warn(message.format(autosign_file))
|
||||
log.warn(message.format(signing_file))
|
||||
return False
|
||||
|
||||
with salt.utils.fopen(autosign_file, 'r') as fp_:
|
||||
with salt.utils.fopen(signing_file, 'r') as fp_:
|
||||
for line in fp_:
|
||||
line = line.strip()
|
||||
|
||||
|
@ -1817,14 +1811,32 @@ class ClearFuncs(object):
|
|||
except re.error:
|
||||
log.warn(
|
||||
'{0} is not a valid regular expression, ignoring line '
|
||||
'in {1}'.format(
|
||||
line, autosign_file
|
||||
)
|
||||
'in {1}'.format(line, signing_file)
|
||||
)
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def _check_autoreject(self, keyid):
|
||||
'''
|
||||
Checks if the specified keyid should automatically be rejected.
|
||||
'''
|
||||
return self._check_signing_file(
|
||||
keyid,
|
||||
self.opts.get('autoreject_file', None)
|
||||
)
|
||||
|
||||
def _check_autosign(self, keyid):
|
||||
'''
|
||||
Checks if the specified keyid should automatically be signed.
|
||||
'''
|
||||
if self.opts['auto_accept']:
|
||||
return True
|
||||
return self._check_signing_file(
|
||||
keyid,
|
||||
self.opts.get('autosign_file', None)
|
||||
)
|
||||
|
||||
def _auth(self, load):
|
||||
'''
|
||||
Authenticate the client, use the sent public key to encrypt the AES key
|
||||
|
@ -1848,6 +1860,11 @@ class ClearFuncs(object):
|
|||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
log.info('Authentication request from {id}'.format(**load))
|
||||
|
||||
# Check if key is configured to be auto-rejected/signed
|
||||
auto_reject = self._check_autoreject(load['id'])
|
||||
auto_sign = self._check_autosign(load['id'])
|
||||
|
||||
pubfn = os.path.join(self.opts['pki_dir'],
|
||||
'minions',
|
||||
load['id'])
|
||||
|
@ -1864,106 +1881,145 @@ class ClearFuncs(object):
|
|||
elif os.path.isfile(pubfn_rejected):
|
||||
# The key has been rejected, don't place it in pending
|
||||
log.info('Public key rejected for {id}'.format(**load))
|
||||
ret = {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
eload = {'result': False,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return ret
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
|
||||
elif os.path.isfile(pubfn):
|
||||
# The key has been accepted check it
|
||||
# The key has been accepted, check it
|
||||
if salt.utils.fopen(pubfn, 'r').read() != load['pub']:
|
||||
log.error(
|
||||
'Authentication attempt from {id} failed, the public '
|
||||
'keys did not match. This may be an attempt to compromise '
|
||||
'the Salt cluster.'.format(**load)
|
||||
)
|
||||
ret = {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
eload = {'result': False,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return ret
|
||||
elif not os.path.isfile(pubfn_pend)\
|
||||
and not self._check_autosign(load['id']):
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
|
||||
elif not os.path.isfile(pubfn_pend):
|
||||
# The key has not been accepted, this is a new minion
|
||||
if os.path.isdir(pubfn_pend):
|
||||
# The key path is a directory, error out
|
||||
log.info(
|
||||
'New public key id is a directory {id}'.format(**load)
|
||||
'New public key {id} is a directory'.format(**load)
|
||||
)
|
||||
ret = {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
eload = {'result': False,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
|
||||
if auto_reject:
|
||||
key_path = pubfn_rejected
|
||||
log.info('New public key for {id} rejected via autoreject_file'
|
||||
.format(**load))
|
||||
key_act = 'reject'
|
||||
key_result = False
|
||||
elif not auto_sign:
|
||||
key_path = pubfn_pend
|
||||
log.info('New public key for {id} placed in pending'
|
||||
.format(**load))
|
||||
key_act = 'pend'
|
||||
key_result = True
|
||||
else:
|
||||
# The key is being automatically accepted, don't do anything
|
||||
# here and let the auto accept logic below handle it.
|
||||
key_path = None
|
||||
|
||||
if key_path is not None:
|
||||
# Write the key to the appropriate location
|
||||
with salt.utils.fopen(key_path, 'w+') as fp_:
|
||||
fp_.write(load['pub'])
|
||||
ret = {'enc': 'clear',
|
||||
'load': {'ret': key_result}}
|
||||
eload = {'result': key_result,
|
||||
'act': key_act,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return ret
|
||||
# This is a new key, stick it in pre
|
||||
log.info(
|
||||
'New public key placed in pending for {id}'.format(**load)
|
||||
)
|
||||
with salt.utils.fopen(pubfn_pend, 'w+') as fp_:
|
||||
fp_.write(load['pub'])
|
||||
ret = {'enc': 'clear',
|
||||
'load': {'ret': True}}
|
||||
eload = {'result': True,
|
||||
'act': 'pend',
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return ret
|
||||
elif os.path.isfile(pubfn_pend)\
|
||||
and not self._check_autosign(load['id']):
|
||||
# This key is in pending, if it is the same key ret True, else
|
||||
# ret False
|
||||
if salt.utils.fopen(pubfn_pend, 'r').read() != load['pub']:
|
||||
log.error(
|
||||
'Authentication attempt from {id} failed, the public '
|
||||
'keys in pending did not match. This may be an attempt to '
|
||||
'compromise the Salt cluster.'.format(**load)
|
||||
)
|
||||
|
||||
elif os.path.isfile(pubfn_pend):
|
||||
# This key is in the pending dir and is awaiting acceptance
|
||||
if auto_reject:
|
||||
# We don't care if the keys match, this minion is being
|
||||
# auto-rejected. Move the key file from the pending dir to the
|
||||
# rejected dir.
|
||||
try:
|
||||
shutil.move(pubfn_pend, pubfn_rejected)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
log.info('Pending public key for {id} rejected via '
|
||||
'autoreject_file'.format(**load))
|
||||
ret = {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
eload = {'result': False,
|
||||
'act': 'reject',
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
return ret
|
||||
|
||||
elif not auto_sign:
|
||||
# This key is in the pending dir and is not being auto-signed.
|
||||
# Check if the keys are the same and error out if this is the
|
||||
# case. Otherwise log the fact that the minion is still
|
||||
# pending.
|
||||
if salt.utils.fopen(pubfn_pend, 'r').read() != load['pub']:
|
||||
log.error(
|
||||
'Authentication attempt from {id} failed, the public '
|
||||
'key in pending did not match. This may be an '
|
||||
'attempt to compromise the Salt cluster.'
|
||||
.format(**load)
|
||||
)
|
||||
eload = {'result': False,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
else:
|
||||
log.info(
|
||||
'Authentication failed from host {id}, the key is in '
|
||||
'pending and needs to be accepted with salt-key '
|
||||
'-a {id}'.format(**load)
|
||||
)
|
||||
eload = {'result': True,
|
||||
'act': 'pend',
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': True}}
|
||||
else:
|
||||
log.info(
|
||||
'Authentication failed from host {id}, the key is in '
|
||||
'pending and needs to be accepted with salt-key '
|
||||
'-a {id}'.format(**load)
|
||||
)
|
||||
eload = {'result': True,
|
||||
'act': 'pend',
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': True}}
|
||||
elif os.path.isfile(pubfn_pend)\
|
||||
and self._check_autosign(load['id']):
|
||||
# This key is in pending, if it is the same key auto accept it
|
||||
if salt.utils.fopen(pubfn_pend, 'r').read() != load['pub']:
|
||||
log.error(
|
||||
'Authentication attempt from {id} failed, the public '
|
||||
'keys in pending did not match. This may be an attempt to '
|
||||
'compromise the Salt cluster.'.format(**load)
|
||||
)
|
||||
eload = {'result': False,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
else:
|
||||
pass
|
||||
elif not os.path.isfile(pubfn_pend)\
|
||||
and self._check_autosign(load['id']):
|
||||
# This is a new key and it should be automatically be accepted
|
||||
pass
|
||||
# This key is in pending and has been configured to be
|
||||
# auto-signed. Check to see if it is the same key, and if
|
||||
# so, pass on doing anything here, and let it get automatically
|
||||
# accepted below.
|
||||
if salt.utils.fopen(pubfn_pend, 'r').read() != load['pub']:
|
||||
log.error(
|
||||
'Authentication attempt from {id} failed, the public '
|
||||
'keys in pending did not match. This may be an '
|
||||
'attempt to compromise the Salt cluster.'
|
||||
.format(**load)
|
||||
)
|
||||
eload = {'result': False,
|
||||
'id': load['id'],
|
||||
'pub': load['pub']}
|
||||
self.event.fire_event(eload, tagify(prefix='auth'))
|
||||
return {'enc': 'clear',
|
||||
'load': {'ret': False}}
|
||||
else:
|
||||
pass
|
||||
|
||||
else:
|
||||
# Something happened that I have not accounted for, FAIL!
|
||||
log.warn('Unaccounted for authentication failure')
|
||||
|
@ -2597,12 +2653,12 @@ class ClearFuncs(object):
|
|||
# touching this stuff, we can probably do what you want to do another
|
||||
# way that won't have a negative impact.
|
||||
load = {
|
||||
'fun': clear_load['fun'],
|
||||
'arg': clear_load['arg'],
|
||||
'tgt': clear_load['tgt'],
|
||||
'jid': clear_load['jid'],
|
||||
'ret': clear_load['ret'],
|
||||
}
|
||||
'fun': clear_load['fun'],
|
||||
'arg': clear_load['arg'],
|
||||
'tgt': clear_load['tgt'],
|
||||
'jid': clear_load['jid'],
|
||||
'ret': clear_load['ret'],
|
||||
}
|
||||
|
||||
if 'id' in extra:
|
||||
load['id'] = extra['id']
|
||||
|
|
Loading…
Add table
Reference in a new issue