Merge pull request #8734 from terminalmage/autoreject_file

add an autoreject_file parameter to the master config file
This commit is contained in:
Thomas S Hatch 2013-12-30 09:24:03 -08:00
commit b3b6c0bcee
4 changed files with 192 additions and 118 deletions

View file

@ -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

View file

@ -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``

View file

@ -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

View file

@ -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']