cinder/cinder/keymgr/migration.py
Alan Bishop 189a1096da Migrate fixed_key encryption keys to Barbican
Assist users who are switching from the legacy ConfKeyManager to
Barbican by automatically migrating any existing keys. Key migration
is executed in its own thread spawned on cinder-volume startup. Two
factors are used to determine whether existing keys need to be
migrated.

1) The ConfKeyManager's fixed_key config value is set (not None).
   This indicates volumes may exist that were encrypted using the
   ConfKeyManager.
2) Barbican is the current key manager.

When the both conditions are met, each instance of the cinder-volume
service scans its volumes in the database, looking for volumes using
the ConfKeyManager's all-zeros encryption key ID. If a volume has an
all-zeros key ID, the same secret (derived from the fixed_key) is stored
in Barbican, and all database references to that volume's key ID are
replaced with the new Barbican key ID.

Implements: blueprint migrate-fixed-key-to-barbican
Change-Id: Ic70f45762cf4e426c222415e49b947a328282ca0
2017-12-01 15:30:32 -05:00

163 lines
6.7 KiB
Python

# Copyright 2017 Red Hat, Inc.
# All Rights Reserved.
#
# 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 binascii
from oslo_config import cfg
from oslo_log import log as logging
from barbicanclient import client as barbican_client
from castellan import options as castellan_options
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import session as ks_session
from cinder import context
from cinder import coordination
from cinder import objects
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
MAX_KEY_MIGRATION_ERRORS = 3
class KeyMigrator(object):
def __init__(self, conf):
self.conf = conf
self.admin_context = context.get_admin_context()
self.fixed_key_id = '00000000-0000-0000-0000-000000000000'
self.fixed_key_bytes = None
self.fixed_key_length = None
def handle_key_migration(self, volumes):
castellan_options.set_defaults(self.conf)
self.conf.import_opt(name='fixed_key',
module_str='cinder.keymgr.conf_key_mgr',
group='key_manager')
fixed_key = self.conf.key_manager.fixed_key
backend = self.conf.key_manager.backend or ''
backend = backend.split('.')[-1]
if backend == 'ConfKeyManager':
LOG.info("Not migrating encryption keys because the "
"ConfKeyManager is still in use.")
elif not fixed_key:
LOG.info("Not migrating encryption keys because the "
"ConfKeyManager's fixed_key is not in use.")
elif backend != 'barbican' and backend != 'BarbicanKeyManager':
# Note: There are two ways of specifying the Barbican backend.
# The long-hand method contains the "BarbicanKeyManager" class
# name, and the short-hand method is just "barbican" with no
# module path prefix.
LOG.warning("Not migrating encryption keys because migration to "
"the '%s' key_manager backend is not supported.",
backend)
self._log_migration_status()
elif not volumes:
LOG.info("Not migrating encryption keys because there are no "
"volumes associated with this host.")
self._log_migration_status()
else:
self.fixed_key_bytes = bytes(binascii.unhexlify(fixed_key))
self.fixed_key_length = len(self.fixed_key_bytes) * 8
self._migrate_keys(volumes)
self._log_migration_status()
def _migrate_keys(self, volumes):
LOG.info("Starting migration of ConfKeyManager keys.")
# Establish a Barbican client session that will be used for the entire
# key migration process. Use cinder's own service credentials.
try:
ks_loading.register_auth_conf_options(self.conf,
'keystone_authtoken')
auth = ks_loading.load_auth_from_conf_options(self.conf,
'keystone_authtoken')
sess = ks_session.Session(auth=auth)
self.barbican = barbican_client.Client(session=sess)
except Exception as e:
LOG.error("Aborting encryption key migration due to "
"error creating Barbican client: %s", e)
return
errors = 0
for volume in volumes:
try:
self._migrate_volume_key(volume)
except Exception as e:
LOG.error("Error migrating encryption key: %s", e)
# NOTE(abishop): There really shouldn't be any soft errors, so
# if an error occurs migrating one key then chances are they
# will all fail. This avoids filling the log with the same
# error in situations where there are many keys to migrate.
errors += 1
if errors > MAX_KEY_MIGRATION_ERRORS:
LOG.error("Aborting encryption key migration "
"(too many errors).")
break
@coordination.synchronized('{volume.id}-{f_name}')
def _migrate_volume_key(self, volume):
if volume.encryption_key_id == self.fixed_key_id:
self._update_encryption_key_id(volume)
def _update_encryption_key_id(self, volume):
LOG.info("Migrating volume %s encryption key to Barbican", volume.id)
# Create a Barbican secret using the same fixed_key algorithm.
secret = self.barbican.secrets.create(algorithm='AES',
bit_length=self.fixed_key_length,
secret_type='symmetric',
mode=None,
payload=self.fixed_key_bytes)
secret_ref = secret.store()
# Create a Barbican ACL so the volume's user can access the secret.
acl = self.barbican.acls.create(entity_ref=secret_ref,
users=[volume.user_id])
acl.submit()
_, _, encryption_key_id = secret_ref.rpartition('/')
volume.encryption_key_id = encryption_key_id
volume.save()
# TODO(abishop): need to determine if any snapshot creations are
# in-flight that might be added to the db with the volume's old
# fixed key ID.
snapshots = objects.snapshot.SnapshotList.get_all_for_volume(
self.admin_context,
volume.id)
for snapshot in snapshots:
snapshot.encryption_key_id = encryption_key_id
snapshot.save()
def _log_migration_status(self):
num_to_migrate = len(objects.volume.VolumeList.get_all(
context=self.admin_context,
filters={'encryption_key_id': self.fixed_key_id}))
if num_to_migrate == 0:
LOG.info("No volumes are using the ConfKeyManager's "
"encryption_key_id.")
else:
LOG.warning("There are still %d volume(s) using the "
"ConfKeyManager's all-zeros encryption key ID.",
num_to_migrate)
def migrate_fixed_key(volumes, conf=CONF):
KeyMigrator(conf).handle_key_migration(volumes)