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:
Thomas Jackson 2015-01-02 16:07:38 -08:00
parent fc06a8bbc8
commit e4556d74cb
3 changed files with 55 additions and 86 deletions

View File

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

View File

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

View File

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