Make a FernetUtils class

By converting our module-level fernet utilities to a class, we can
extend it or modify it in ways to make the utilities work for outside
the fernet token provider. This change is in preparation to use fernet
to encrypt credentials at rest.

bp credential-encryption

Change-Id: Ia4e9fd2b8597993f006d9fea82b782085f2cdbc2
This commit is contained in:
Werner Mendizabal 2016-08-10 21:13:19 +00:00 committed by Steve Martinelli
parent 96f9d00702
commit bc95434472
6 changed files with 240 additions and 224 deletions

View File

@ -524,12 +524,13 @@ class FernetSetup(BasePermissionsSetup):
@classmethod @classmethod
def main(cls): def main(cls):
from keystone.common import fernet_utils as fernet from keystone.common import fernet_utils as utils
fernet_utils = utils.FernetUtils()
keystone_user_id, keystone_group_id = cls.get_user_group() keystone_user_id, keystone_group_id = cls.get_user_group()
fernet.create_key_directory(keystone_user_id, keystone_group_id) fernet_utils.create_key_directory(keystone_user_id, keystone_group_id)
if fernet.validate_key_repository(requires_write=True): if fernet_utils.validate_key_repository(requires_write=True):
fernet.initialize_key_repository( fernet_utils.initialize_key_repository(
keystone_user_id, keystone_group_id) keystone_user_id, keystone_group_id)
@ -555,11 +556,12 @@ class FernetRotate(BasePermissionsSetup):
@classmethod @classmethod
def main(cls): def main(cls):
from keystone.common import fernet_utils as fernet from keystone.common import fernet_utils as utils
fernet_utils = utils.FernetUtils()
keystone_user_id, keystone_group_id = cls.get_user_group() keystone_user_id, keystone_group_id = cls.get_user_group()
if fernet.validate_key_repository(requires_write=True): if fernet_utils.validate_key_repository(requires_write=True):
fernet.rotate_keys(keystone_user_id, keystone_group_id) fernet_utils.rotate_keys(keystone_user_id, keystone_group_id)
class TokenFlush(BaseApp): class TokenFlush(BaseApp):

View File

@ -12,7 +12,7 @@
import keystone.conf import keystone.conf
from keystone.common import fernet_utils from keystone.common import fernet_utils as utils
CONF = keystone.conf.CONF CONF = keystone.conf.CONF
@ -25,6 +25,7 @@ def symptom_usability_of_Fernet_key_repository():
keystone, but not world-readable, because it contains security-sensitive keystone, but not world-readable, because it contains security-sensitive
secrets. secrets.
""" """
fernet_utils = utils.FernetUtils()
return ( return (
'fernet' in CONF.token.provider 'fernet' in CONF.token.provider
and not fernet_utils.validate_key_repository()) and not fernet_utils.validate_key_repository())
@ -38,6 +39,7 @@ def symptom_keys_in_Fernet_key_repository():
with keys, and periodically rotate your keys with `keystone-manage with keys, and periodically rotate your keys with `keystone-manage
fernet_rotate`. fernet_rotate`.
""" """
fernet_utils = utils.FernetUtils()
return ( return (
'fernet' in CONF.token.provider 'fernet' in CONF.token.provider
and not fernet_utils.load_keys()) and not fernet_utils.load_keys())

View File

@ -25,10 +25,12 @@ LOG = log.getLogger(__name__)
CONF = keystone.conf.CONF CONF = keystone.conf.CONF
def validate_key_repository(requires_write=False): class FernetUtils(object):
def validate_key_repository(self, requires_write=False):
"""Validate permissions on the key repository directory.""" """Validate permissions on the key repository directory."""
# NOTE(lbragstad): We shouldn't need to check if the directory was passed # NOTE(lbragstad): We shouldn't need to check if the directory was
# in as None because we don't set allow_no_values to True. # passed in as None because we don't set allow_no_values to True.
# ensure current user has sufficient access to the key repository # ensure current user has sufficient access to the key repository
is_valid = (os.access(CONF.fernet_tokens.key_repository, os.R_OK) and is_valid = (os.access(CONF.fernet_tokens.key_repository, os.R_OK) and
@ -40,8 +42,8 @@ def validate_key_repository(requires_write=False):
if not is_valid: if not is_valid:
LOG.error( LOG.error(
_LE('Either [fernet_tokens] key_repository does not exist or ' _LE('Either [fernet_tokens] key_repository does not exist or '
'Keystone does not have sufficient permission to access it: ' 'Keystone does not have sufficient permission to access '
'%s'), CONF.fernet_tokens.key_repository) 'it: %s'), CONF.fernet_tokens.key_repository)
else: else:
# ensure the key repository isn't world-readable # ensure the key repository isn't world-readable
stat_info = os.stat(CONF.fernet_tokens.key_repository) stat_info = os.stat(CONF.fernet_tokens.key_repository)
@ -53,12 +55,11 @@ def validate_key_repository(requires_write=False):
return is_valid return is_valid
def _convert_to_integers(self, id_value):
def _convert_to_integers(id_value):
"""Cast user and group system identifiers to integers.""" """Cast user and group system identifiers to integers."""
# NOTE(lbragstad) os.chown() will raise a TypeError here if # NOTE(lbragstad) os.chown() will raise a TypeError here if
# keystone_user_id and keystone_group_id are not integers. Let's # keystone_user_id and keystone_group_id are not integers. Let's cast
# cast them to integers if we can because it's possible to pass non-integer # them to integers if we can because it's possible to pass non-integer
# values into the fernet_setup utility. # values into the fernet_setup utility.
try: try:
id_int = int(id_value) id_int = int(id_value)
@ -69,9 +70,9 @@ def _convert_to_integers(id_value):
return id_int return id_int
def create_key_directory(self, keystone_user_id=None,
def create_key_directory(keystone_user_id=None, keystone_group_id=None): keystone_group_id=None):
"""If the configured key directory does not exist, attempt to create it.""" """Attempt to create the key directory if it doesn't exist."""
if not os.access(CONF.fernet_tokens.key_repository, os.F_OK): if not os.access(CONF.fernet_tokens.key_repository, os.F_OK):
LOG.info(_LI( LOG.info(_LI(
'[fernet_tokens] key_repository does not appear to exist; ' '[fernet_tokens] key_repository does not appear to exist; '
@ -81,9 +82,9 @@ def create_key_directory(keystone_user_id=None, keystone_group_id=None):
os.makedirs(CONF.fernet_tokens.key_repository, 0o700) os.makedirs(CONF.fernet_tokens.key_repository, 0o700)
except OSError: except OSError:
LOG.error(_LE( LOG.error(_LE(
'Failed to create [fernet_tokens] key_repository: either it ' 'Failed to create [fernet_tokens] key_repository: either'
'already exists or you don\'t have sufficient permissions to ' 'it already exists or you don\'t have sufficient '
'create it')) 'permissions to create it'))
if keystone_user_id and keystone_group_id: if keystone_user_id and keystone_group_id:
os.chown( os.chown(
@ -93,15 +94,15 @@ def create_key_directory(keystone_user_id=None, keystone_group_id=None):
elif keystone_user_id or keystone_group_id: elif keystone_user_id or keystone_group_id:
LOG.warning(_LW( LOG.warning(_LW(
'Unable to change the ownership of [fernet_tokens] ' 'Unable to change the ownership of [fernet_tokens] '
'key_repository without a keystone user ID and keystone group ' 'key_repository without a keystone user ID and keystone '
'ID both being provided: %s') % 'group ID both being provided: %s') %
CONF.fernet_tokens.key_repository) CONF.fernet_tokens.key_repository)
def _create_new_key(self, keystone_user_id, keystone_group_id):
def _create_new_key(keystone_user_id, keystone_group_id):
"""Securely create a new encryption key. """Securely create a new encryption key.
Create a new key that is readable by the Keystone group and Keystone user. Create a new key that is readable by the Keystone group and Keystone
user.
""" """
key = fernet.Fernet.generate_key() # key is bytes key = fernet.Fernet.generate_key() # key is bytes
@ -114,18 +115,20 @@ def _create_new_key(keystone_user_id, keystone_group_id):
os.seteuid(keystone_user_id) os.seteuid(keystone_user_id)
elif keystone_user_id or keystone_group_id: elif keystone_user_id or keystone_group_id:
LOG.warning(_LW( LOG.warning(_LW(
'Unable to change the ownership of the new key without a keystone ' 'Unable to change the ownership of the new key without a '
'user ID and keystone group ID both being provided: %s') % 'keystone user ID and keystone group ID both being provided: '
'%s') %
CONF.fernet_tokens.key_repository) CONF.fernet_tokens.key_repository)
# Determine the file name of the new key # Determine the file name of the new key
key_file = os.path.join(CONF.fernet_tokens.key_repository, '0') key_file = os.path.join(CONF.fernet_tokens.key_repository, '0')
try: try:
with open(key_file, 'w') as f: with open(key_file, 'w') as f:
f.write(key.decode('utf-8')) # convert key to str for the file. # convert key to str for the file.
f.write(key.decode('utf-8'))
finally: finally:
# After writing the key, set the umask back to it's original value. Do # After writing the key, set the umask back to it's original value.
# the same with group and user identifiers if a Keystone group or user # Do the same with group and user identifiers if a Keystone group
# was supplied. # or user was supplied.
os.umask(old_umask) os.umask(old_umask)
if keystone_user_id and keystone_group_id: if keystone_user_id and keystone_group_id:
os.seteuid(old_euid) os.seteuid(old_euid)
@ -133,8 +136,8 @@ def _create_new_key(keystone_user_id, keystone_group_id):
LOG.info(_LI('Created a new key: %s'), key_file) LOG.info(_LI('Created a new key: %s'), key_file)
def initialize_key_repository(self, keystone_user_id=None,
def initialize_key_repository(keystone_user_id=None, keystone_group_id=None): keystone_group_id=None):
"""Create a key repository and bootstrap it with a key. """Create a key repository and bootstrap it with a key.
:param keystone_user_id: User ID of the Keystone user. :param keystone_user_id: User ID of the Keystone user.
@ -148,13 +151,12 @@ def initialize_key_repository(keystone_user_id=None, keystone_group_id=None):
return return
# bootstrap an existing key # bootstrap an existing key
_create_new_key(keystone_user_id, keystone_group_id) self._create_new_key(keystone_user_id, keystone_group_id)
# ensure that we end up with a primary and secondary key # ensure that we end up with a primary and secondary key
rotate_keys(keystone_user_id, keystone_group_id) self.rotate_keys(keystone_user_id, keystone_group_id)
def rotate_keys(self, keystone_user_id=None, keystone_group_id=None):
def rotate_keys(keystone_user_id=None, keystone_group_id=None):
"""Create a new primary key and revoke excess active keys. """Create a new primary key and revoke excess active keys.
:param keystone_user_id: User ID of the Keystone user. :param keystone_user_id: User ID of the Keystone user.
@ -162,30 +164,33 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None):
Key rotation utilizes the following behaviors: Key rotation utilizes the following behaviors:
- The highest key number is used as the primary key (used for encryption). - The highest key number is used as the primary key (used for
encryption).
- All keys can be used for decryption. - All keys can be used for decryption.
- New keys are always created as key "0," which serves as a placeholder - New keys are always created as key "0," which serves as a placeholder
before promoting it to be the primary key. before promoting it to be the primary key.
This strategy allows you to safely perform rotation on one node in a This strategy allows you to safely perform rotation on one node in a
cluster, before syncing the results of the rotation to all other nodes cluster, before syncing the results of the rotation to all other nodes
(during both key rotation and synchronization, all nodes must recognize all (during both key rotation and synchronization, all nodes must recognize
primary keys). all primary keys).
""" """
# read the list of key files # read the list of key files
key_files = dict() key_files = dict()
for filename in os.listdir(CONF.fernet_tokens.key_repository): for filename in os.listdir(CONF.fernet_tokens.key_repository):
path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) path = os.path.join(CONF.fernet_tokens.key_repository,
str(filename))
if os.path.isfile(path): if os.path.isfile(path):
try: try:
key_id = int(filename) key_id = int(filename)
except ValueError: # nosec : name isn't a number, ignore the file. except ValueError: # nosec : name isn't a number
pass pass
else: else:
key_files[key_id] = path key_files[key_id] = path
LOG.info(_LI('Starting key rotation with %(count)s key files: %(list)s'), { LOG.info(_LI('Starting key rotation with %(count)s key files: '
'%(list)s'), {
'count': len(key_files), 'count': len(key_files),
'list': list(key_files.values())}) 'list': list(key_files.values())})
@ -198,7 +203,8 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None):
# promote the next primary key to be the primary # promote the next primary key to be the primary
os.rename( os.rename(
os.path.join(CONF.fernet_tokens.key_repository, '0'), os.path.join(CONF.fernet_tokens.key_repository, '0'),
os.path.join(CONF.fernet_tokens.key_repository, str(new_primary_key))) os.path.join(CONF.fernet_tokens.key_repository,
str(new_primary_key)))
key_files.pop(0) key_files.pop(0)
key_files[new_primary_key] = os.path.join( key_files[new_primary_key] = os.path.join(
CONF.fernet_tokens.key_repository, CONF.fernet_tokens.key_repository,
@ -206,14 +212,14 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None):
LOG.info(_LI('Promoted key 0 to be the primary: %s'), new_primary_key) LOG.info(_LI('Promoted key 0 to be the primary: %s'), new_primary_key)
# add a new key to the rotation, which will be the *next* primary # add a new key to the rotation, which will be the *next* primary
_create_new_key(keystone_user_id, keystone_group_id) self._create_new_key(keystone_user_id, keystone_group_id)
max_active_keys = CONF.fernet_tokens.max_active_keys max_active_keys = CONF.fernet_tokens.max_active_keys
# purge excess keys # purge excess keys
# Note that key_files doesn't contain the new active key that was created, # Note that key_files doesn't contain the new active key that was
# only the old active keys. # created, only the old active keys.
keys = sorted(key_files.keys(), reverse=True) keys = sorted(key_files.keys(), reverse=True)
while len(keys) > (max_active_keys - 1): while len(keys) > (max_active_keys - 1):
index_to_purge = keys.pop() index_to_purge = keys.pop()
@ -221,8 +227,7 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None):
LOG.info(_LI('Excess key to purge: %s'), key_to_purge) LOG.info(_LI('Excess key to purge: %s'), key_to_purge)
os.remove(key_to_purge) os.remove(key_to_purge)
def load_keys(self):
def load_keys():
"""Load keys from disk into a list. """Load keys from disk into a list.
The first key in the list is the primary key used for encryption. All The first key in the list is the primary key used for encryption. All
@ -230,32 +235,33 @@ def load_keys():
tokens. tokens.
""" """
if not validate_key_repository(): if not self.validate_key_repository():
return [] return []
# build a dictionary of key_number:encryption_key pairs # build a dictionary of key_number:encryption_key pairs
keys = dict() keys = dict()
for filename in os.listdir(CONF.fernet_tokens.key_repository): for filename in os.listdir(CONF.fernet_tokens.key_repository):
path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) path = os.path.join(CONF.fernet_tokens.key_repository,
str(filename))
if os.path.isfile(path): if os.path.isfile(path):
with open(path, 'r') as key_file: with open(path, 'r') as key_file:
try: try:
key_id = int(filename) key_id = int(filename)
except ValueError: # nosec : filename isn't a number, ignore except ValueError: # nosec : filename isn't a number,
# this file since it's not a key. # ignore this file since it's not a key.
pass pass
else: else:
keys[key_id] = key_file.read() keys[key_id] = key_file.read()
if len(keys) != CONF.fernet_tokens.max_active_keys: if len(keys) != CONF.fernet_tokens.max_active_keys:
# If there haven't been enough key rotations to reach max_active_keys, # If there haven't been enough key rotations to reach
# or if the configured value of max_active_keys has changed since the # max_active_keys, or if the configured value of max_active_keys
# last rotation, then reporting the discrepancy might be useful. Once # has changed since the last rotation, then reporting the
# the number of keys matches max_active_keys, this log entry is too # discrepancy might be useful. Once the number of keys matches
# repetitive to be useful. # max_active_keys, this log entry is too repetitive to be useful.
LOG.info(_LI( LOG.info(_LI(
'Loaded %(count)d encryption keys (max_active_keys=%(max)d) from: ' 'Loaded %(count)d encryption keys (max_active_keys=%(max)d) '
'%(dir)s'), { 'from: %(dir)s'), {
'count': len(keys), 'count': len(keys),
'max': CONF.fernet_tokens.max_active_keys, 'max': CONF.fernet_tokens.max_active_keys,
'dir': CONF.fernet_tokens.key_repository}) 'dir': CONF.fernet_tokens.key_repository})

View File

@ -26,5 +26,6 @@ class KeyRepository(fixtures.Fixture):
self.config_fixture.config(group='fernet_tokens', self.config_fixture.config(group='fernet_tokens',
key_repository=directory) key_repository=directory)
utils.create_key_directory() fernet_utils = utils.FernetUtils()
utils.initialize_key_repository() fernet_utils.create_key_directory()
fernet_utils.initialize_key_repository()

View File

@ -507,7 +507,8 @@ class TestFernetKeyRotation(unit.TestCase):
""" """
# Load the keys into a list, keys is list of six.text_type. # Load the keys into a list, keys is list of six.text_type.
keys = fernet_utils.load_keys() utils = fernet_utils.FernetUtils()
keys = utils.load_keys()
# Sort the list of keys by the keys themselves (they were previously # Sort the list of keys by the keys themselves (they were previously
# sorted by filename). # sorted by filename).
@ -543,6 +544,7 @@ class TestFernetKeyRotation(unit.TestCase):
# support max_active_keys being set any lower. # support max_active_keys being set any lower.
min_active_keys = 2 min_active_keys = 2
utils = fernet_utils.FernetUtils()
# Simulate every rotation strategy up to "rotating once a week while # Simulate every rotation strategy up to "rotating once a week while
# maintaining a year's worth of keys." # maintaining a year's worth of keys."
for max_active_keys in range(min_active_keys, 52 + 1): for max_active_keys in range(min_active_keys, 52 + 1):
@ -565,7 +567,7 @@ class TestFernetKeyRotation(unit.TestCase):
# Rotate the keys just enough times to fully populate the key # Rotate the keys just enough times to fully populate the key
# repository. # repository.
for rotation in range(max_active_keys - min_active_keys): for rotation in range(max_active_keys - min_active_keys):
fernet_utils.rotate_keys() utils.rotate_keys()
self.assertRepositoryState(expected_size=rotation + 3) self.assertRepositoryState(expected_size=rotation + 3)
exp_keys.append(next_key_number) exp_keys.append(next_key_number)
@ -578,7 +580,7 @@ class TestFernetKeyRotation(unit.TestCase):
# Rotate an additional number of times to ensure that we maintain # Rotate an additional number of times to ensure that we maintain
# the desired number of active keys. # the desired number of active keys.
for rotation in range(10): for rotation in range(10):
fernet_utils.rotate_keys() utils.rotate_keys()
self.assertRepositoryState(expected_size=max_active_keys) self.assertRepositoryState(expected_size=max_active_keys)
exp_keys.pop(1) exp_keys.pop(1)
@ -591,7 +593,8 @@ class TestFernetKeyRotation(unit.TestCase):
evil_file = os.path.join(CONF.fernet_tokens.key_repository, '99.bak') evil_file = os.path.join(CONF.fernet_tokens.key_repository, '99.bak')
with open(evil_file, 'w'): with open(evil_file, 'w'):
pass pass
fernet_utils.rotate_keys() utils = fernet_utils.FernetUtils()
utils.rotate_keys()
self.assertTrue(os.path.isfile(evil_file)) self.assertTrue(os.path.isfile(evil_file))
keys = 0 keys = 0
for x in os.listdir(CONF.fernet_tokens.key_repository): for x in os.listdir(CONF.fernet_tokens.key_repository):
@ -607,6 +610,7 @@ class TestLoadKeys(unit.TestCase):
evil_file = os.path.join(CONF.fernet_tokens.key_repository, '~1') evil_file = os.path.join(CONF.fernet_tokens.key_repository, '~1')
with open(evil_file, 'w'): with open(evil_file, 'w'):
pass pass
keys = fernet_utils.load_keys() utils = fernet_utils.FernetUtils()
keys = utils.load_keys()
self.assertEqual(2, len(keys)) self.assertEqual(2, len(keys))
self.assertTrue(len(keys[0])) self.assertTrue(len(keys[0]))

View File

@ -57,7 +57,8 @@ class TokenFormatter(object):
``encrypt(plaintext)`` and ``decrypt(ciphertext)``. ``encrypt(plaintext)`` and ``decrypt(ciphertext)``.
""" """
keys = utils.load_keys() fernet_utils = utils.FernetUtils()
keys = fernet_utils.load_keys()
if not keys: if not keys:
raise exception.KeysNotFound() raise exception.KeysNotFound()