keystone  18.0.0
About: OpenStack Keystone (Core Service: Identity) provides an authentication and authorization service for other OpenStack services. Provides a catalog of endpoints for all OpenStack services.
The "Victoria" series (maintained release).
  Fossies Dox: keystone-18.0.0.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

fernet_utils.py
Go to the documentation of this file.
1 # Licensed under the Apache License, Version 2.0 (the "License"); you may
2 # not use this file except in compliance with the License. You may obtain
3 # a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 # License for the specific language governing permissions and limitations
11 # under the License.
12 
13 import base64
14 import os
15 import stat
16 
17 from cryptography import fernet
18 from oslo_log import log
19 
20 from keystone.common import utils
21 import keystone.conf
22 
23 
24 LOG = log.getLogger(__name__)
25 
26 CONF = keystone.conf.CONF
27 
28 # NOTE(lbragstad): In the event there are no encryption keys on disk, let's use
29 # a default one until a proper key repository is set up. This allows operators
30 # to gracefully upgrade from Mitaka to Newton without a key repository,
31 # especially in multi-node deployments. The NULL_KEY is specific to credential
32 # encryption only and has absolutely no beneficial purpose outside of easing
33 # upgrades.
34 NULL_KEY = base64.urlsafe_b64encode(b'\x00' * 32)
35 
36 
37 class FernetUtils(object):
38 
39  def __init__(self, key_repository=None, max_active_keys=None,
40  config_group=None):
41  self.key_repository = key_repository
42  self.max_active_keys = max_active_keys
43  self.config_group = config_group
44 
45  def validate_key_repository(self, requires_write=False):
46  """Validate permissions on the key repository directory."""
47  # NOTE(lbragstad): We shouldn't need to check if the directory was
48  # passed in as None because we don't set allow_no_values to True.
49 
50  # ensure current user has sufficient access to the key repository
51  is_valid = (os.access(self.key_repository, os.R_OK) and
52  os.access(self.key_repository, os.X_OK))
53  if requires_write:
54  is_valid = (is_valid and
55  os.access(self.key_repository, os.W_OK))
56 
57  if not is_valid:
58  LOG.error(
59  'Either [%(config_group)s] key_repository does not exist '
60  'or Keystone does not have sufficient permission to '
61  'access it: %(key_repo)s',
62  {'key_repo': self.key_repository,
63  'config_group': self.config_group})
64  else:
65  # ensure the key repository isn't world-readable
66  stat_info = os.stat(self.key_repository)
67  if(stat_info.st_mode & stat.S_IROTH or
68  stat_info.st_mode & stat.S_IXOTH):
69  LOG.warning(
70  'key_repository is world readable: %s',
71  self.key_repository)
72 
73  return is_valid
74 
75  def create_key_directory(self, keystone_user_id=None,
76  keystone_group_id=None):
77  """Attempt to create the key directory if it doesn't exist."""
78  utils.create_directory(
79  self.key_repository, keystone_user_id=keystone_user_id,
80  keystone_group_id=keystone_group_id
81  )
82 
83  def _create_new_key(self, keystone_user_id, keystone_group_id):
84  """Securely create a new encryption key.
85 
86  Create a new key that is readable by the Keystone group and Keystone
87  user.
88 
89  To avoid disk write failure, this function will create a tmp key file
90  first, and then rename it as the valid new key.
91  """
92  self._create_tmp_new_key(keystone_user_id, keystone_group_id)
94 
95  def _create_tmp_new_key(self, keystone_user_id, keystone_group_id):
96  """Securely create a new tmp encryption key.
97 
98  This created key is not effective until _become_valid_new_key().
99  """
100  key = fernet.Fernet.generate_key() # key is bytes
101 
102  # This ensures the key created is not world-readable
103  old_umask = os.umask(0o177)
104  if keystone_user_id and keystone_group_id:
105  old_egid = os.getegid()
106  old_euid = os.geteuid()
107  os.setegid(keystone_group_id)
108  os.seteuid(keystone_user_id)
109  elif keystone_user_id or keystone_group_id:
110  LOG.warning(
111  'Unable to change the ownership of the new key without a '
112  'keystone user ID and keystone group ID both being provided: '
113  '%s', self.key_repository)
114  # Determine the file name of the new key
115  key_file = os.path.join(self.key_repository, '0.tmp')
116  create_success = False
117  try:
118  with open(key_file, 'w') as f:
119  # convert key to str for the file.
120  f.write(key.decode('utf-8'))
121  f.flush()
122  create_success = True
123  except IOError:
124  LOG.error('Failed to create new temporary key: %s', key_file)
125  raise
126  finally:
127  # After writing the key, set the umask back to it's original value.
128  # Do the same with group and user identifiers if a Keystone group
129  # or user was supplied.
130  os.umask(old_umask)
131  if keystone_user_id and keystone_group_id:
132  os.seteuid(old_euid)
133  os.setegid(old_egid)
134  # Deal with the tmp key file
135  if not create_success and os.access(key_file, os.F_OK):
136  os.remove(key_file)
137 
138  LOG.info('Created a new temporary key: %s', key_file)
139 
141  """Make the tmp new key a valid new key.
142 
143  The tmp new key must be created by _create_tmp_new_key().
144  """
145  tmp_key_file = os.path.join(self.key_repository, '0.tmp')
146  valid_key_file = os.path.join(self.key_repository, '0')
147 
148  os.rename(tmp_key_file, valid_key_file)
149 
150  LOG.info('Become a valid new key: %s', valid_key_file)
151 
152  def _get_key_files(self, key_repo):
153  key_files = dict()
154  keys = dict()
155  for filename in os.listdir(key_repo):
156  path = os.path.join(key_repo, str(filename))
157  if os.path.isfile(path):
158  with open(path, 'r') as key_file:
159  try:
160  key_id = int(filename)
161  except ValueError: # nosec : name is not a number
162  pass
163  else:
164  key = key_file.read()
165  if len(key) == 0:
166  LOG.warning('Ignoring empty key found in key '
167  'repository: %s', path)
168  continue
169  key_files[key_id] = path
170  keys[key_id] = key
171  return key_files, keys
172 
173  def initialize_key_repository(self, keystone_user_id=None,
174  keystone_group_id=None):
175  """Create a key repository and bootstrap it with a key.
176 
177  :param keystone_user_id: User ID of the Keystone user.
178  :param keystone_group_id: Group ID of the Keystone user.
179 
180  """
181  # make sure we have work to do before proceeding
182  if os.access(os.path.join(self.key_repository, '0'),
183  os.F_OK):
184  LOG.info('Key repository is already initialized; aborting.')
185  return
186 
187  # bootstrap an existing key
188  self._create_new_key(keystone_user_id, keystone_group_id)
189 
190  # ensure that we end up with a primary and secondary key
191  self.rotate_keys(keystone_user_id, keystone_group_id)
192 
193  def rotate_keys(self, keystone_user_id=None, keystone_group_id=None):
194  """Create a new primary key and revoke excess active keys.
195 
196  :param keystone_user_id: User ID of the Keystone user.
197  :param keystone_group_id: Group ID of the Keystone user.
198 
199  Key rotation utilizes the following behaviors:
200 
201  - The highest key number is used as the primary key (used for
202  encryption).
203  - All keys can be used for decryption.
204  - New keys are always created as key "0," which serves as a placeholder
205  before promoting it to be the primary key.
206 
207  This strategy allows you to safely perform rotation on one node in a
208  cluster, before syncing the results of the rotation to all other nodes
209  (during both key rotation and synchronization, all nodes must recognize
210  all primary keys).
211 
212  """
213  # read the list of key files
214  key_files, _ = self._get_key_files(self.key_repository)
215 
216  LOG.info('Starting key rotation with %(count)s key files: '
217  '%(list)s', {
218  'count': len(key_files),
219  'list': list(key_files.values())})
220 
221  # add a tmp new key to the rotation, which will be the *next* primary
222  self._create_tmp_new_key(keystone_user_id, keystone_group_id)
223 
224  # determine the number of the new primary key
225  current_primary_key = max(key_files.keys())
226  LOG.info('Current primary key is: %s', current_primary_key)
227  new_primary_key = current_primary_key + 1
228  LOG.info('Next primary key will be: %s', new_primary_key)
229 
230  # promote the next primary key to be the primary
231  os.rename(
232  os.path.join(self.key_repository, '0'),
233  os.path.join(self.key_repository, str(new_primary_key))
234  )
235  key_files.pop(0)
236  key_files[new_primary_key] = os.path.join(
237  self.key_repository,
238  str(new_primary_key))
239  LOG.info('Promoted key 0 to be the primary: %s', new_primary_key)
240 
241  # rename the tmp key to the real staged key
242  self._become_valid_new_key()
243 
244  max_active_keys = self.max_active_keys
245 
246  # purge excess keys
247 
248  # Note that key_files doesn't contain the new active key that was
249  # created, only the old active keys.
250  keys = sorted(key_files.keys(), reverse=True)
251  while len(keys) > (max_active_keys - 1):
252  index_to_purge = keys.pop()
253  key_to_purge = key_files[index_to_purge]
254  LOG.info('Excess key to purge: %s', key_to_purge)
255  os.remove(key_to_purge)
256 
257  def load_keys(self, use_null_key=False):
258  """Load keys from disk into a list.
259 
260  The first key in the list is the primary key used for encryption. All
261  other keys are active secondary keys that can be used for decrypting
262  tokens.
263 
264  :param use_null_key: If true, a known key containing null bytes will be
265  appended to the list of returned keys.
266 
267  """
268  if not self.validate_key_repository():
269  if use_null_key:
270  return [NULL_KEY]
271  return []
272 
273  # build a dictionary of key_number:encryption_key pairs
274  _, keys = self._get_key_files(self.key_repository)
275 
276  if len(keys) != self.max_active_keys:
277  # Once the number of keys matches max_active_keys, this log entry
278  # is too repetitive to be useful. Also note that it only makes
279  # sense to log this message for tokens since credentials doesn't
280  # have a `max_active_key` configuration option.
281  if self.key_repository == CONF.fernet_tokens.key_repository:
282  msg = ('Loaded %(count)d Fernet keys from %(dir)s, but '
283  '`[fernet_tokens] max_active_keys = %(max)d`; perhaps '
284  'there have not been enough key rotations to reach '
285  '`max_active_keys` yet?')
286  LOG.debug(msg, {
287  'count': len(keys),
288  'max': self.max_active_keys,
289  'dir': self.key_repository})
290 
291  # return the encryption_keys, sorted by key number, descending
292  key_list = [keys[x] for x in sorted(keys.keys(), reverse=True)]
293 
294  if use_null_key:
295  key_list.append(NULL_KEY)
296 
297  return key_list
keystone.common.fernet_utils.FernetUtils.max_active_keys
max_active_keys
Definition: fernet_utils.py:42
keystone.common.fernet_utils.FernetUtils._get_key_files
def _get_key_files(self, key_repo)
Definition: fernet_utils.py:152
keystone.common.fernet_utils.FernetUtils._become_valid_new_key
def _become_valid_new_key(self)
Definition: fernet_utils.py:140
keystone.common.fernet_utils.FernetUtils.validate_key_repository
def validate_key_repository(self, requires_write=False)
Definition: fernet_utils.py:45
keystone.common.fernet_utils.FernetUtils._create_new_key
def _create_new_key(self, keystone_user_id, keystone_group_id)
Definition: fernet_utils.py:83
keystone.common.fernet_utils.FernetUtils.key_repository
key_repository
Definition: fernet_utils.py:41
keystone.common.fernet_utils.FernetUtils._create_tmp_new_key
def _create_tmp_new_key(self, keystone_user_id, keystone_group_id)
Definition: fernet_utils.py:95
keystone.common.fernet_utils.FernetUtils.load_keys
def load_keys(self, use_null_key=False)
Definition: fernet_utils.py:257
keystone.common.fernet_utils.FernetUtils.rotate_keys
def rotate_keys(self, keystone_user_id=None, keystone_group_id=None)
Definition: fernet_utils.py:193
keystone.common.fernet_utils.FernetUtils.initialize_key_repository
def initialize_key_repository(self, keystone_user_id=None, keystone_group_id=None)
Definition: fernet_utils.py:174
keystone.conf
Definition: __init__.py:1
keystone.common.fernet_utils.FernetUtils.create_key_directory
def create_key_directory(self, keystone_user_id=None, keystone_group_id=None)
Definition: fernet_utils.py:76
keystone.common.fernet_utils.FernetUtils.__init__
def __init__(self, key_repository=None, max_active_keys=None, config_group=None)
Definition: fernet_utils.py:40
keystone.common
Definition: __init__.py:1
keystone.common.fernet_utils.FernetUtils.config_group
config_group
Definition: fernet_utils.py:43
keystone.common.fernet_utils.FernetUtils
Definition: fernet_utils.py:37