mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
Make AES key never hit disk on the master
In the past we've been doing coordination of the aes key using dropfiles etc. Not only is this significantly more costly (at least 1 stat per reqserver request) it also means that the symmetric key we use to pub/req messages has been on disk. This re-works the aes key to be a multiprocessing.Array() (char array) which is shared amongst the processes. Now the dropfile is just a request for the master to rotate the key, and this means that *all* key rotation on the master will generate the appropriate event (instead of just ones who passed in sock_dir to dropfile())
This commit is contained in:
parent
fc06a8bbc8
commit
e4556d74cb
@ -2056,8 +2056,6 @@ def apply_master_config(overrides=None, defaults=None):
|
||||
if len(opts['sock_dir']) > len(opts['cachedir']) + 10:
|
||||
opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix')
|
||||
|
||||
opts['aes'] = salt.crypt.Crypticle.generate_key_string()
|
||||
|
||||
opts['extension_modules'] = (
|
||||
opts.get('extension_modules') or
|
||||
os.path.join(opts['cachedir'], 'extmods')
|
||||
|
@ -42,59 +42,28 @@ log = logging.getLogger(__name__)
|
||||
|
||||
def dropfile(cachedir, user=None, sock_dir=None):
|
||||
'''
|
||||
Set an AES dropfile to update the publish session key
|
||||
|
||||
A dropfile is checked periodically by master workers to determine
|
||||
if AES key rotation has occurred.
|
||||
Set an AES dropfile to request the master update the publish session key
|
||||
'''
|
||||
dfnt = os.path.join(cachedir, '.dfnt')
|
||||
dfn = os.path.join(cachedir, '.dfn')
|
||||
|
||||
def ready():
|
||||
'''
|
||||
Because MWorker._update_aes uses second-precision mtime
|
||||
to detect changes to the file, we must avoid writing two
|
||||
versions with the same mtime.
|
||||
|
||||
Note that this only makes rapid updates in serial safe: concurrent
|
||||
updates could still both pass this check and then write two different
|
||||
keys with the same mtime.
|
||||
'''
|
||||
try:
|
||||
stats = os.stat(dfn)
|
||||
except os.error:
|
||||
# Not there, go ahead and write it
|
||||
return True
|
||||
else:
|
||||
if stats.st_mtime == time.time():
|
||||
# The mtime is the current time, we must
|
||||
# wait until time has moved on.
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
while not ready():
|
||||
log.warning('Waiting before writing {0}'.format(dfn))
|
||||
time.sleep(1)
|
||||
|
||||
log.info('Rotating AES key')
|
||||
aes = Crypticle.generate_key_string()
|
||||
# set a mask (to avoid a race condition on file creation) and store original.
|
||||
mask = os.umask(191)
|
||||
with salt.utils.fopen(dfnt, 'w+') as fp_:
|
||||
fp_.write(aes)
|
||||
if user:
|
||||
try:
|
||||
import pwd
|
||||
uid = pwd.getpwnam(user).pw_uid
|
||||
os.chown(dfnt, uid, -1)
|
||||
except (KeyError, ImportError, OSError, IOError):
|
||||
pass
|
||||
try:
|
||||
log.info('Rotating AES key')
|
||||
|
||||
shutil.move(dfnt, dfn)
|
||||
os.umask(mask)
|
||||
with salt.utils.fopen(dfn, 'w+') as fp_:
|
||||
fp_.write('')
|
||||
if user:
|
||||
try:
|
||||
import pwd
|
||||
uid = pwd.getpwnam(user).pw_uid
|
||||
os.chown(dfn, uid, -1)
|
||||
except (KeyError, ImportError, OSError, IOError):
|
||||
pass
|
||||
finally:
|
||||
os.umask(mask) # restore original umask
|
||||
if sock_dir:
|
||||
event = salt.utils.event.SaltEvent('master', sock_dir)
|
||||
event.fire_event({'rotate_aes_key': True}, tag='key')
|
||||
# TODO: deprecation warning
|
||||
pass
|
||||
|
||||
|
||||
def gen_keys(keydir, keyname, keysize, user=None):
|
||||
@ -745,7 +714,8 @@ class Crypticle(object):
|
||||
SIG_SIZE = hashlib.sha256().digest_size
|
||||
|
||||
def __init__(self, opts, key_string, key_size=192):
|
||||
self.keys = self.extract_keys(key_string, key_size)
|
||||
self.key_string = key_string
|
||||
self.keys = self.extract_keys(self.key_string, key_size)
|
||||
self.key_size = key_size
|
||||
self.serial = salt.payload.Serial(opts)
|
||||
|
||||
|
@ -7,6 +7,7 @@ involves preparing the three listeners and the workers needed by the master.
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import python libs
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
@ -78,6 +79,7 @@ class SMaster(object):
|
||||
:param dict opts: The salt options dictionary
|
||||
'''
|
||||
self.opts = opts
|
||||
self.opts['aes'] = multiprocessing.Array(ctypes.c_char, salt.crypt.Crypticle.generate_key_string())
|
||||
self.master_key = salt.crypt.MasterKeys(self.opts)
|
||||
self.key = self.__prep_key()
|
||||
self.crypticle = self.__prep_crypticle()
|
||||
@ -86,7 +88,7 @@ class SMaster(object):
|
||||
'''
|
||||
Return the crypticle used for AES
|
||||
'''
|
||||
return salt.crypt.Crypticle(self.opts, self.opts['aes'])
|
||||
return salt.crypt.Crypticle(self.opts, self.opts['aes'].value)
|
||||
|
||||
def __prep_key(self):
|
||||
'''
|
||||
@ -226,20 +228,36 @@ class Maintenance(multiprocessing.Process):
|
||||
|
||||
def handle_key_rotate(self, now):
|
||||
'''
|
||||
Rotate the AES key on a schedule
|
||||
Rotate the AES key rotation
|
||||
'''
|
||||
to_rotate = False
|
||||
dfn = os.path.join(self.opts['cachedir'], '.dfn')
|
||||
try:
|
||||
stats = os.stat(dfn)
|
||||
if stats.st_mode == 0o100400:
|
||||
to_rotate = True
|
||||
else:
|
||||
log.error('Found dropfile with incorrect permissions, ignoring...')
|
||||
os.remove(dfn)
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
|
||||
if self.opts.get('publish_session'):
|
||||
if now - self.rotate >= self.opts['publish_session']:
|
||||
salt.crypt.dropfile(
|
||||
self.opts['cachedir'],
|
||||
self.opts['user'],
|
||||
self.opts['sock_dir'])
|
||||
self.rotate = now
|
||||
if self.opts.get('ping_on_rotate'):
|
||||
# Ping all minions to get them to pick up the new key
|
||||
log.debug('Pinging all connected minions '
|
||||
'due to AES key rotation')
|
||||
salt.utils.master.ping_all_connected_minions(self.opts)
|
||||
to_rotate = True
|
||||
|
||||
if to_rotate:
|
||||
# should be unecessary-- since no one else should be modifying
|
||||
with self.opts['aes'].get_lock():
|
||||
self.opts['aes'].value = salt.crypt.Crypticle.generate_key_string()
|
||||
self.event.fire_event({'rotate_aes_key': True}, tag='key')
|
||||
self.rotate = now
|
||||
if self.opts.get('ping_on_rotate'):
|
||||
# Ping all minions to get them to pick up the new key
|
||||
log.debug('Pinging all connected minions '
|
||||
'due to AES key rotation')
|
||||
salt.utils.master.ping_all_connected_minions(self.opts)
|
||||
|
||||
def handle_pillargit(self):
|
||||
'''
|
||||
@ -809,27 +827,10 @@ class MWorker(multiprocessing.Process):
|
||||
Check to see if a fresh AES key is available and update the components
|
||||
of the worker
|
||||
'''
|
||||
dfn = os.path.join(self.opts['cachedir'], '.dfn')
|
||||
try:
|
||||
stats = os.stat(dfn)
|
||||
except os.error:
|
||||
return
|
||||
if stats.st_mode != 0o100400:
|
||||
# Invalid dfn, return
|
||||
return
|
||||
if stats.st_mtime > self.k_mtime:
|
||||
# new key, refresh crypticle
|
||||
with salt.utils.fopen(dfn) as fp_:
|
||||
aes = fp_.read()
|
||||
if len(aes) != 76:
|
||||
return
|
||||
log.debug('New master AES key found by pid {0}'.format(os.getpid()))
|
||||
self.crypticle = salt.crypt.Crypticle(self.opts, aes)
|
||||
if self.opts['aes'].value != self.crypticle.key_string:
|
||||
self.crypticle = salt.crypt.Crypticle(self.opts, self.opts['aes'].value)
|
||||
self.clear_funcs.crypticle = self.crypticle
|
||||
self.clear_funcs.opts['aes'] = aes
|
||||
self.aes_funcs.crypticle = self.crypticle
|
||||
self.aes_funcs.opts['aes'] = aes
|
||||
self.k_mtime = stats.st_mtime
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
@ -1845,13 +1846,13 @@ class ClearFuncs(object):
|
||||
if 'token' in load:
|
||||
try:
|
||||
mtoken = self.master_key.key.private_decrypt(load['token'], 4)
|
||||
aes = '{0}_|-{1}'.format(self.opts['aes'], mtoken)
|
||||
aes = '{0}_|-{1}'.format(self.opts['aes'].value, mtoken)
|
||||
except Exception:
|
||||
# Token failed to decrypt, send back the salty bacon to
|
||||
# support older minions
|
||||
pass
|
||||
else:
|
||||
aes = self.opts['aes']
|
||||
aes = self.opts['aes'].value
|
||||
|
||||
ret['aes'] = pub.public_encrypt(aes, 4)
|
||||
else:
|
||||
@ -1866,8 +1867,8 @@ class ClearFuncs(object):
|
||||
# support older minions
|
||||
pass
|
||||
|
||||
aes = self.opts['aes']
|
||||
ret['aes'] = pub.public_encrypt(self.opts['aes'], 4)
|
||||
aes = self.opts['aes'].value
|
||||
ret['aes'] = pub.public_encrypt(self.opts['aes'].value, 4)
|
||||
# Be aggressive about the signature
|
||||
digest = hashlib.sha256(aes).hexdigest()
|
||||
ret['sig'] = self.master_key.key.private_encrypt(digest, 5)
|
||||
|
Loading…
Reference in New Issue
Block a user