diff options
Diffstat (limited to 'keystone-moon/keystone/token/providers/fernet/utils.py')
-rw-r--r-- | keystone-moon/keystone/token/providers/fernet/utils.py | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/keystone-moon/keystone/token/providers/fernet/utils.py b/keystone-moon/keystone/token/providers/fernet/utils.py new file mode 100644 index 00000000..56624ee5 --- /dev/null +++ b/keystone-moon/keystone/token/providers/fernet/utils.py @@ -0,0 +1,243 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import stat + +from cryptography import fernet +from oslo_config import cfg +from oslo_log import log + +from keystone.i18n import _LE, _LW, _LI + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + + +def validate_key_repository(): + """Validate permissions on the key repository directory.""" + # NOTE(lbragstad): We shouldn't need to check if the directory was passed + # in as None because we don't set allow_no_values to True. + + # ensure current user has full access to the key repository + if (not os.access(CONF.fernet_tokens.key_repository, os.R_OK) or not + os.access(CONF.fernet_tokens.key_repository, os.W_OK) or not + os.access(CONF.fernet_tokens.key_repository, os.X_OK)): + LOG.error( + _LE('Either [fernet_tokens] key_repository does not exist or ' + 'Keystone does not have sufficient permission to access it: ' + '%s'), CONF.fernet_tokens.key_repository) + return False + + # ensure the key repository isn't world-readable + stat_info = os.stat(CONF.fernet_tokens.key_repository) + if stat_info.st_mode & stat.S_IROTH or stat_info.st_mode & stat.S_IXOTH: + LOG.warning(_LW( + '[fernet_tokens] key_repository is world readable: %s'), + CONF.fernet_tokens.key_repository) + + return True + + +def _convert_to_integers(id_value): + """Cast user and group system identifiers to integers.""" + # NOTE(lbragstad) os.chown() will raise a TypeError here if + # keystone_user_id and keystone_group_id are not integers. Let's + # cast them to integers if we can because it's possible to pass non-integer + # values into the fernet_setup utility. + try: + id_int = int(id_value) + except ValueError as e: + msg = ('Unable to convert Keystone user or group ID. Error: %s', e) + LOG.error(msg) + raise + + return id_int + + +def create_key_directory(keystone_user_id=None, keystone_group_id=None): + """If the configured key directory does not exist, attempt to create it.""" + if not os.access(CONF.fernet_tokens.key_repository, os.F_OK): + LOG.info(_LI( + '[fernet_tokens] key_repository does not appear to exist; ' + 'attempting to create it')) + + try: + os.makedirs(CONF.fernet_tokens.key_repository, 0o700) + except OSError: + LOG.error(_LE( + 'Failed to create [fernet_tokens] key_repository: either it ' + 'already exists or you don\'t have sufficient permissions to ' + 'create it')) + + if keystone_user_id and keystone_group_id: + os.chown( + CONF.fernet_tokens.key_repository, + keystone_user_id, + keystone_group_id) + elif keystone_user_id or keystone_group_id: + LOG.warning(_LW( + 'Unable to change the ownership of [fernet_tokens] ' + 'key_repository without a keystone user ID and keystone group ' + 'ID both being provided: %s') % + CONF.fernet_tokens.key_repository) + + +def _create_new_key(keystone_user_id, keystone_group_id): + """Securely create a new encryption key. + + Create a new key that is readable by the Keystone group and Keystone user. + """ + key = fernet.Fernet.generate_key() + + # This ensures the key created is not world-readable + old_umask = os.umask(0o177) + if keystone_user_id and keystone_group_id: + old_egid = os.getegid() + old_euid = os.geteuid() + os.setegid(keystone_group_id) + os.seteuid(keystone_user_id) + elif keystone_user_id or keystone_group_id: + LOG.warning(_LW( + 'Unable to change the ownership of the new key without a keystone ' + 'user ID and keystone group ID both being provided: %s') % + CONF.fernet_tokens.key_repository) + # Determine the file name of the new key + key_file = os.path.join(CONF.fernet_tokens.key_repository, '0') + try: + with open(key_file, 'w') as f: + f.write(key) + finally: + # After writing the key, set the umask back to it's original value. Do + # the same with group and user identifiers if a Keystone group or user + # was supplied. + os.umask(old_umask) + if keystone_user_id and keystone_group_id: + os.seteuid(old_euid) + os.setegid(old_egid) + + LOG.info(_LI('Created a new key: %s'), key_file) + + +def initialize_key_repository(keystone_user_id=None, keystone_group_id=None): + """Create a key repository and bootstrap it with a key. + + :param keystone_user_id: User ID of the Keystone user. + :param keystone_group_id: Group ID of the Keystone user. + + """ + # make sure we have work to do before proceeding + if os.access(os.path.join(CONF.fernet_tokens.key_repository, '0'), + os.F_OK): + LOG.info(_LI('Key repository is already initialized; aborting.')) + return + + # bootstrap an existing key + _create_new_key(keystone_user_id, keystone_group_id) + + # ensure that we end up with a primary and secondary key + rotate_keys(keystone_user_id, keystone_group_id) + + +def rotate_keys(keystone_user_id=None, keystone_group_id=None): + """Create a new primary key and revoke excess active keys. + + :param keystone_user_id: User ID of the Keystone user. + :param keystone_group_id: Group ID of the Keystone user. + + Key rotation utilizes the following behaviors: + + - The highest key number is used as the primary key (used for encryption). + - All keys can be used for decryption. + - New keys are always created as key "0," which serves as a placeholder + before promoting it to be the primary key. + + 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 + (during both key rotation and synchronization, all nodes must recognize all + primary keys). + + """ + # read the list of key files + key_files = dict() + for filename in os.listdir(CONF.fernet_tokens.key_repository): + path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) + if os.path.isfile(path): + key_files[int(filename)] = path + + LOG.info(_LI('Starting key rotation with %(count)s key files: %(list)s'), { + 'count': len(key_files), + 'list': key_files.values()}) + + # determine the number of the new primary key + current_primary_key = max(key_files.keys()) + LOG.info(_LI('Current primary key is: %s'), current_primary_key) + new_primary_key = current_primary_key + 1 + LOG.info(_LI('Next primary key will be: %s'), new_primary_key) + + # promote the next primary key to be the primary + os.rename( + os.path.join(CONF.fernet_tokens.key_repository, '0'), + os.path.join(CONF.fernet_tokens.key_repository, str(new_primary_key))) + key_files.pop(0) + key_files[new_primary_key] = os.path.join( + CONF.fernet_tokens.key_repository, + str(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 + _create_new_key(keystone_user_id, keystone_group_id) + + # check for bad configuration + if CONF.fernet_tokens.max_active_keys < 1: + LOG.warning(_LW( + '[fernet_tokens] max_active_keys must be at least 1 to maintain a ' + 'primary key.')) + CONF.fernet_tokens.max_active_keys = 1 + + # purge excess keys + keys = sorted(key_files.keys()) + excess_keys = ( + keys[:len(key_files) - CONF.fernet_tokens.max_active_keys + 1]) + LOG.info(_LI('Excess keys to purge: %s'), excess_keys) + for i in excess_keys: + os.remove(key_files[i]) + + +def load_keys(): + """Load keys from disk into a list. + + The first key in the list is the primary key used for encryption. All + other keys are active secondary keys that can be used for decrypting + tokens. + + """ + if not validate_key_repository(): + return [] + + # build a dictionary of key_number:encryption_key pairs + keys = dict() + for filename in os.listdir(CONF.fernet_tokens.key_repository): + path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) + if os.path.isfile(path): + with open(path, 'r') as key_file: + keys[int(filename)] = key_file.read() + + LOG.info(_LI( + 'Loaded %(count)s encryption keys from: %(dir)s'), { + 'count': len(keys), + 'dir': CONF.fernet_tokens.key_repository}) + + # return the encryption_keys, sorted by key number, descending + return [keys[x] for x in sorted(keys.keys(), reverse=True)] |