Passphrase protect master private key

This commit is contained in:
Sigmon, Jeffrey 2017-06-23 18:09:56 -05:00 committed by Elastic Cloud User
parent f3051a355b
commit bb29afd022
6 changed files with 90 additions and 22 deletions

View File

@ -301,6 +301,22 @@
##### Security settings #####
##########################################
# Enable passphrase protection of Master private key. Although a string value
# is acceptable; passwords should be stored in an external vaulting mechanism
# and retrieved via sdb. See https://docs.saltstack.com/en/latest/topics/sdb/.
# Passphrase protection is off by default but an example of an sdb profile and
# query is as follows.
# masterkeyring:
# driver: keyring
# service: system
#
# key_pass: sdb://masterkeyring/key_pass
# Enable passphrase protection of the Master signing_key. This only applies if
# master_sign_pubkey is set to True. This is disabled by default.
# master_sign_pubkey: True
# signing_key_pass: sdb://masterkeyring/signing_pass
# Enable "open mode", this mode still maintains encryption, but turns off
# authentication, this is only intended for highly secure environments or for
# the situation where your keys end up in a bad state. If you run in open mode

View File

@ -458,6 +458,12 @@ VALID_OPTS = {
# Allow a daemon to function even if the key directories are not secured
'permissive_pki_access': bool,
# The passphrase of the master's private key
'key_pass': str,
# The passphrase of the master's private signing key
'signing_key_pass': str,
# The path to a directory to pull in configuration file includes
'default_include': str,
@ -1557,6 +1563,8 @@ DEFAULT_MASTER_OPTS = {
'key_logfile': os.path.join(salt.syspaths.LOGS_DIR, 'key'),
'verify_env': True,
'permissive_pki_access': False,
'key_pass': None,
'signing_key_pass': None,
'default_include': 'master.d/*.conf',
'winrepo_dir': os.path.join(salt.syspaths.BASE_FILE_ROOTS_DIR, 'win', 'repo'),
'winrepo_dir_ng': os.path.join(salt.syspaths.BASE_FILE_ROOTS_DIR, 'win', 'repo-ng'),

View File

@ -54,10 +54,11 @@ import salt.payload
import salt.transport.client
import salt.transport.frame
import salt.utils.rsax931
import salt.utils.sdb
import salt.utils.verify
import salt.version
from salt.exceptions import (
AuthenticationError, SaltClientError, SaltReqTimeoutError
AuthenticationError, SaltClientError, SaltReqTimeoutError, MasterExit
)
import tornado.gen
@ -94,7 +95,7 @@ def dropfile(cachedir, user=None):
os.umask(mask) # restore original umask
def gen_keys(keydir, keyname, keysize, user=None):
def gen_keys(keydir, keyname, keysize, user=None, passphrase=None):
'''
Generate a RSA public keypair for use with salt
@ -102,6 +103,7 @@ def gen_keys(keydir, keyname, keysize, user=None):
:param str keyname: The type of salt server for whom this key should be written. (i.e. 'master' or 'minion')
:param int keysize: The number of bits in the key
:param str user: The user on the system who should own this keypair
:param str passphrase: The passphrase which should be used to encrypt the private key
:rtype: str
:return: Path on the filesystem to the RSA private key
@ -123,7 +125,7 @@ def gen_keys(keydir, keyname, keysize, user=None):
cumask = os.umask(191)
with salt.utils.files.fopen(priv, 'wb+') as f:
f.write(gen.exportKey('PEM'))
f.write(gen.exportKey('PEM', passphrase))
os.umask(cumask)
with salt.utils.files.fopen(pub, 'wb+') as f:
f.write(gen.publickey().exportKey('PEM'))
@ -142,7 +144,7 @@ def gen_keys(keydir, keyname, keysize, user=None):
@salt.utils.decorators.memoize
def _get_key_with_evict(path, timestamp):
def _get_key_with_evict(path, timestamp, passphrase):
'''
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
@ -152,11 +154,11 @@ def _get_key_with_evict(path, timestamp):
'''
log.debug('salt.crypt._get_key_with_evict: Loading private key')
with salt.utils.files.fopen(path) as f:
key = RSA.importKey(f.read())
key = RSA.importKey(f.read(), passphrase)
return key
def _get_rsa_key(path):
def _get_rsa_key(path, passphrase):
'''
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
@ -168,14 +170,14 @@ def _get_rsa_key(path):
retrieve the key from disk.
'''
log.debug('salt.crypt._get_rsa_key: Loading private key')
return _get_key_with_evict(path, str(os.path.getmtime(path)))
return _get_key_with_evict(path, str(os.path.getmtime(path)), passphrase)
def sign_message(privkey_path, message):
def sign_message(privkey_path, message, passphrase=None):
'''
Use Crypto.Signature.PKCS1_v1_5 to sign a message. Returns the signature.
'''
key = _get_rsa_key(privkey_path)
key = _get_rsa_key(privkey_path, passphrase)
log.debug('salt.crypt.sign_message: Signing message.')
signer = PKCS1_v1_5.new(key)
return signer.sign(SHA.new(message))
@ -194,7 +196,7 @@ def verify_signature(pubkey_path, message, signature):
return verifier.verify(SHA.new(message), signature)
def gen_signature(priv_path, pub_path, sign_path):
def gen_signature(priv_path, pub_path, sign_path, passphrase=None):
'''
creates a signature for the given public-key with
the given private key and writes it to sign_path
@ -203,7 +205,7 @@ def gen_signature(priv_path, pub_path, sign_path):
with salt.utils.files.fopen(pub_path) as fp_:
mpub_64 = fp_.read()
mpub_sig = sign_message(priv_path, mpub_64)
mpub_sig = sign_message(priv_path, mpub_64, passphrase)
mpub_sig_64 = binascii.b2a_base64(mpub_sig)
if os.path.isfile(sign_path):
return False
@ -261,7 +263,9 @@ class MasterKeys(dict):
self.pub_path = os.path.join(self.opts['pki_dir'], 'master.pub')
self.rsa_path = os.path.join(self.opts['pki_dir'], 'master.pem')
self.key = self.__get_keys()
key_pass = salt.utils.sdb.sdb_get(self.opts['key_pass'], self.opts)
self.key = self.__get_keys(passphrase=key_pass)
self.pub_signature = None
# set names for the signing key-pairs
@ -288,11 +292,15 @@ class MasterKeys(dict):
# create a new signing key-pair to sign the masters
# auth-replies when a minion tries to connect
else:
key_pass = salt.utils.sdb.sdb_get(self.opts['signing_key_pass'], self.opts)
self.pub_sign_path = os.path.join(self.opts['pki_dir'],
opts['master_sign_key_name'] + '.pub')
self.rsa_sign_path = os.path.join(self.opts['pki_dir'],
opts['master_sign_key_name'] + '.pem')
self.sign_key = self.__get_keys(name=opts['master_sign_key_name'])
self.sign_key = self.__get_keys(name=opts['master_sign_key_name'],
passphrase=key_pass)
# We need __setstate__ and __getstate__ to avoid pickling errors since
# some of the member variables correspond to Cython objects which are
@ -305,7 +313,7 @@ class MasterKeys(dict):
def __getstate__(self):
return {'opts': self.opts}
def __get_keys(self, name='master'):
def __get_keys(self, name='master', passphrase=None):
'''
Returns a key object for a key in the pki-dir
'''
@ -313,16 +321,22 @@ class MasterKeys(dict):
name + '.pem')
if os.path.exists(path):
with salt.utils.files.fopen(path) as f:
key = RSA.importKey(f.read())
try:
key = RSA.importKey(f.read(), passphrase)
except ValueError as e:
message = 'Unable to read key: {0}; passphrase may be incorrect'.format(path)
log.error(message)
raise MasterExit(message)
log.debug('Loaded {0} key: {1}'.format(name, path))
else:
log.info('Generating {0} keys: {1}'.format(name, self.opts['pki_dir']))
gen_keys(self.opts['pki_dir'],
name,
self.opts['keysize'],
self.opts.get('user'))
with salt.utils.files.fopen(self.rsa_path) as f:
key = RSA.importKey(f.read())
self.opts.get('user'),
passphrase)
with salt.utils.files.fopen(path) as f:
key = RSA.importKey(f.read(), passphrase)
return key
def get_pub_str(self, name='master'):

View File

@ -27,6 +27,7 @@ import salt.utils.args
import salt.utils.event
import salt.utils.files
import salt.utils.kinds
import salt.utils.sdb
# pylint: disable=import-error,no-name-in-module,redefined-builtin
import salt.ext.six as six
@ -376,6 +377,8 @@ class Key(object):
io_loop=io_loop
)
self.passphrase = salt.utils.sdb.sdb_get(self.opts['signing_key_pass'], self.opts)
def _check_minions_directories(self):
'''
Return the minion keys directory paths
@ -411,7 +414,7 @@ class Key(object):
'''
keydir, keyname, keysize, user = self._get_key_attrs(keydir, keyname,
keysize, user)
salt.crypt.gen_keys(keydir, keyname, keysize, user)
salt.crypt.gen_keys(keydir, keyname, keysize, user, self.passphrase)
return salt.utils.pem_finger(os.path.join(keydir, keyname + '.pub'))
def gen_signature(self, privkey, pubkey, sig_path):
@ -420,7 +423,8 @@ class Key(object):
'''
return salt.crypt.gen_signature(privkey,
pubkey,
sig_path)
sig_path,
self.passphrase)
def gen_keys_signature(self, priv, pub, signature_path, auto_create=False, keysize=None):
'''
@ -454,7 +458,8 @@ class Key(object):
salt.crypt.gen_keys(self.opts['pki_dir'],
self.opts['master_sign_key_name'],
keysize or self.opts['keysize'],
self.opts.get('user'))
self.opts.get('user'),
self.passphrase)
priv = self.opts['pki_dir'] + '/' + self.opts['master_sign_key_name'] + '.pem'
else:

View File

@ -441,9 +441,13 @@ class AESReqServerMixin(object):
else:
# the master has its own signing-keypair, compute the master.pub's
# signature and append that to the auth-reply
# get the key_pass for the signing key
key_pass = salt.utils.sdb.sdb_get(self.opts['signing_key_pass'], self.opts)
log.debug("Signing master public key before sending")
pub_sign = salt.crypt.sign_message(self.master_key.get_sign_paths()[1],
ret['pub_key'])
ret['pub_key'], key_pass)
ret.update({'pub_sig': binascii.b2a_base64(pub_sign)})
mcipher = PKCS1_OAEP.new(self.master_key.key)

View File

@ -103,11 +103,32 @@ class CryptTestCase(TestCase):
crypt.gen_keys('/keydir', 'keyname', 2048)
salt.utils.files.fopen.assert_has_calls([open_priv_wb, open_pub_wb], any_order=True)
@patch('os.umask', MagicMock())
@patch('os.chmod', MagicMock())
@patch('os.chown', MagicMock())
@patch('os.access', MagicMock(return_value=True))
def test_gen_keys_with_passphrase(self):
with patch('salt.utils.fopen', mock_open()):
open_priv_wb = call('/keydir/keyname.pem', 'wb+')
open_pub_wb = call('/keydir/keyname.pub', 'wb+')
with patch('os.path.isfile', return_value=True):
self.assertEqual(crypt.gen_keys('/keydir', 'keyname', 2048, passphrase='password'), '/keydir/keyname.pem')
self.assertNotIn(open_priv_wb, salt.utils.fopen.mock_calls)
self.assertNotIn(open_pub_wb, salt.utils.fopen.mock_calls)
with patch('os.path.isfile', return_value=False):
with patch('salt.utils.fopen', mock_open()):
crypt.gen_keys('/keydir', 'keyname', 2048)
salt.utils.fopen.assert_has_calls([open_priv_wb, open_pub_wb], any_order=True)
def test_sign_message(self):
key = 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_sign_message_with_passphrase(self):
with patch('salt.utils.fopen', mock_open(read_data=PRIVKEY_DATA)):
self.assertEqual(SIG, crypt.sign_message('/keydir/keyname.pem', MSG, passphrase='password'))
def test_verify_signature(self):
with patch('salt.utils.files.fopen', mock_open(read_data=PUBKEY_DATA)):
self.assertTrue(crypt.verify_signature('/keydir/keyname.pub', MSG, SIG))