@ -25,240 +25,246 @@ LOG = log.getLogger(__name__)
CONF = keystone . conf . CONF
def validate_key_repository ( requires_write = False ) :
""" 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 sufficient access to the key repository
is_valid = ( os . access ( CONF . fernet_tokens . key_repository , os . R_OK ) and
os . access ( CONF . fernet_tokens . key_repository , os . X_OK ) )
if requires_write :
is_valid = ( is_valid and
os . access ( CONF . fernet_tokens . key_repository , os . W_OK ) )
if not is_valid :
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 )
else :
# 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 is_valid
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 = _LE ( ' Unable to convert Keystone user or group ID. Error: %s ' )
LOG . error ( msg , e )
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 ' ) )
class FernetUtils ( object ) :
def validate_key_repository ( self , requires_write = False ) :
""" 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 sufficient access to the key repository
is_valid = ( os . access ( CONF . fernet_tokens . key_repository , os . R_OK ) and
os . access ( CONF . fernet_tokens . key_repository , os . X_OK ) )
if requires_write :
is_valid = ( is_valid and
os . access ( CONF . fernet_tokens . key_repository , os . W_OK ) )
if not is_valid :
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 )
else :
# 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 is_valid
def _convert_to_integers ( self , 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 :
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 ' ) )
id_int = int ( id_value )
except ValueError as e :
msg = _LE ( ' Unable to convert Keystone user or group ID. Error: %s ' )
LOG . error ( msg , e )
raise
return id_int
def create_key_directory ( self , keystone_user_id = None ,
keystone_group_id = None ) :
""" Attempt to create the key directory if it doesn ' t exist. """
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 ( self , 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 ( ) # key is bytes
# This ensures the key created is not world-readable
old_umask = os . umask ( 0o177 )
if keystone_user_id and keystone_group_id :
os . chown (
CONF . fernet_tokens . key_repository ,
keystone_user_id ,
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 [fernet_tokens] '
' key_repository without a keystone user ID and keystone group '
' ID both being provided: %s ' ) %
' 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 )
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 ( ) # key is bytes
# 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 . decode ( ' utf-8 ' ) ) # convert key to str for the file.
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 ) :
try :
key_id = int ( filename )
except ValueError : # nosec : name isn't a number, ignore the file.
pass
else :
key_files [ key_id ] = path
LOG . info ( _LI ( ' Starting key rotation with %(count)s key files: %(list)s ' ) , {
' count ' : len ( key_files ) ,
' list ' : 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 )
max_active_keys = CONF . fernet_tokens . max_active_keys
# purge excess keys
# Note that key_files doesn't contain the new active key that was created,
# only the old active keys.
keys = sorted ( key_files . keys ( ) , reverse = True )
while len ( keys ) > ( max_active_keys - 1 ) :
index_to_purge = keys . pop ( )
key_to_purge = key_files [ index_to_purge ]
LOG . info ( _LI ( ' Excess key to purge: %s ' ) , key_to_purge )
os . remove ( key_to_purge )
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 :
# 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 :
# convert key to str for the file.
f . write ( key . decode ( ' utf-8 ' ) )
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 ( self , 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
self . _create_new_key ( keystone_user_id , keystone_group_id )
# ensure that we end up with a primary and secondary key
self . rotate_keys ( keystone_user_id , keystone_group_id )
def rotate_keys ( self , 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 ) :
try :
key_id = int ( filename )
except ValueError : # nosec : filename isn't a number, ignore
# this file since it's not a key.
except ValueError : # nosec : name isn't a number
pass
else :
keys [ key_id ] = key_file . read ( )
if len ( keys ) != CONF . fernet_tokens . max_active_keys :
# If there haven't been enough key rotations to reach max_active_keys,
# or if the configured value of max_active_keys has changed since the
# last rotation, then reporting the discrepancy might be useful. Once
# the number of keys matches max_active_keys, this log entry is too
# repetitive to be useful.
LOG . info ( _LI (
' Loaded %(count)d encryption keys (max_active_keys= %(max)d ) from: '
' %(dir)s ' ) , {
' count ' : len ( keys ) ,
' max ' : CONF . fernet_tokens . max_active_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 ) ]
key_files [ key_id ] = path
LOG . info ( _LI ( ' Starting key rotation with %(count)s key files: '
' %(list)s ' ) , {
' count ' : len ( key_files ) ,
' list ' : 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
self . _create_new_key ( keystone_user_id , keystone_group_id )
max_active_keys = CONF . fernet_tokens . max_active_keys
# purge excess keys
# Note that key_files doesn't contain the new active key that was
# created, only the old active keys.
keys = sorted ( key_files . keys ( ) , reverse = True )
while len ( keys ) > ( max_active_keys - 1 ) :
index_to_purge = keys . pop ( )
key_to_purge = key_files [ index_to_purge ]
LOG . info ( _LI ( ' Excess key to purge: %s ' ) , key_to_purge )
os . remove ( key_to_purge )
def load_keys ( self ) :
""" 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 self . 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 :
try :
key_id = int ( filename )
except ValueError : # nosec : filename isn't a number,
# ignore this file since it's not a key.
pass
else :
keys [ key_id ] = key_file . read ( )
if len ( keys ) != CONF . fernet_tokens . max_active_keys :
# If there haven't been enough key rotations to reach
# max_active_keys, or if the configured value of max_active_keys
# has changed since the last rotation, then reporting the
# discrepancy might be useful. Once the number of keys matches
# max_active_keys, this log entry is too repetitive to be useful.
LOG . info ( _LI (
' Loaded %(count)d encryption keys (max_active_keys= %(max)d ) '
' from: %(dir)s ' ) , {
' count ' : len ( keys ) ,
' max ' : CONF . fernet_tokens . max_active_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 ) ]