Add initial support for Fernet tokens
Starting OpenStack Rocky the currently used `uuid` token format is no longer supported and we need to change to use `fernet` tokens. This change provides basic functionalty to initialize fernet token repository and distribute keys to non-leader units. A configuration option is also added allowing change of token format in a controlled manner prior to upgrading to OpenStack Rocky. Further work is required to implement key rotation, actions etc. and these topics will be addressed in separate commits. The commit also fixes a instance of missing release check for writing of `policy.json`, and a few places where writing of `policy.json` previously was omitted. Change-Id: I1d0ff22a5f091b02f5700412745572c246103e9e
This commit is contained in:
parent
ac40485052
commit
1e991dc28b
12
config.yaml
12
config.yaml
|
@ -96,6 +96,18 @@ options:
|
|||
type: string
|
||||
default: 'Admin'
|
||||
description: Admin role to be associated with admin and service users.
|
||||
token-provider:
|
||||
type: string
|
||||
default: 'uuid'
|
||||
description: |
|
||||
Transitional configuration option to enable migration to Fernet tokens
|
||||
prior to upgrade to OpenStack Rocky.
|
||||
.
|
||||
Supported values are 'uuid' and 'fernet'.
|
||||
.
|
||||
NOTE: This configuration option is honoured on OpenStack versions Ocata
|
||||
through Queens. For OpenStack Rocky it is a unconfigurable default.
|
||||
Silently ignored for all other versions.
|
||||
token-expiration:
|
||||
type: int
|
||||
default: 3600
|
||||
|
|
|
@ -174,6 +174,7 @@ class KeystoneContext(context.OSContextGenerator):
|
|||
|
||||
ctxt['identity_backend'] = config('identity-backend')
|
||||
ctxt['assignment_backend'] = config('assignment-backend')
|
||||
ctxt['token_provider'] = config('token-provider')
|
||||
if config('identity-backend') == 'ldap':
|
||||
ctxt['ldap_server'] = config('ldap-server')
|
||||
ctxt['ldap_user'] = config('ldap-user')
|
||||
|
|
|
@ -101,6 +101,10 @@ from keystone_utils import (
|
|||
ADMIN_PROJECT,
|
||||
create_or_show_domain,
|
||||
restart_keystone,
|
||||
fernet_enabled,
|
||||
fernet_leader_set,
|
||||
fernet_setup,
|
||||
fernet_write_keys,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.hahelpers.cluster import (
|
||||
|
@ -222,6 +226,10 @@ def config_changed_postupgrade():
|
|||
# packages may have changed so ensure they are installed.
|
||||
apt_install(filter_installed_packages(determine_packages()))
|
||||
|
||||
if is_leader() and fernet_enabled():
|
||||
fernet_setup()
|
||||
fernet_leader_set()
|
||||
|
||||
configure_https()
|
||||
open_port(config('service-port'))
|
||||
|
||||
|
@ -235,6 +243,9 @@ def config_changed_postupgrade():
|
|||
if (is_db_initialised() and is_elected_leader(CLUSTER_RES) and not
|
||||
is_unit_paused_set()):
|
||||
ensure_initial_admin(config)
|
||||
if CompareOpenStackReleases(
|
||||
os_release('keystone')) >= 'liberty':
|
||||
CONFIGS.write(POLICY_JSON)
|
||||
|
||||
update_all_identity_relation_units()
|
||||
update_all_domain_backends()
|
||||
|
@ -332,6 +343,9 @@ def leader_init_db_if_ready(use_current_context=False):
|
|||
|
||||
migrate_database()
|
||||
ensure_initial_admin(config)
|
||||
if CompareOpenStackReleases(
|
||||
os_release('keystone')) >= 'liberty':
|
||||
CONFIGS.write(POLICY_JSON)
|
||||
# Ensure any existing service entries are updated in the
|
||||
# new database backend. Also avoid duplicate db ready check.
|
||||
update_all_identity_relation_units(check_db_ready=False)
|
||||
|
@ -483,7 +497,12 @@ def leader_settings_changed():
|
|||
CONFIGS.write(TOKEN_FLUSH_CRON_FILE)
|
||||
|
||||
# Make sure we keep domain and/or project ids used in templates up to date
|
||||
CONFIGS.write(POLICY_JSON)
|
||||
if CompareOpenStackReleases(
|
||||
os_release('keystone')) >= 'liberty':
|
||||
CONFIGS.write(POLICY_JSON)
|
||||
|
||||
if fernet_enabled():
|
||||
fernet_write_keys()
|
||||
|
||||
update_all_identity_relation_units()
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
@ -96,12 +97,14 @@ from charmhelpers.fetch import (
|
|||
)
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
mkdir,
|
||||
service_restart,
|
||||
service_stop,
|
||||
service_start,
|
||||
pwgen,
|
||||
lsb_release,
|
||||
CompareHostReleases,
|
||||
write_file,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.peerstorage import (
|
||||
|
@ -1915,3 +1918,56 @@ def restart_keystone():
|
|||
service_restart('snap.keystone.*')
|
||||
else:
|
||||
service_restart(keystone_service())
|
||||
|
||||
|
||||
def fernet_enabled():
|
||||
"""Helper function for determinining whether Fernet tokens are enabled"""
|
||||
cmp_release = CompareOpenStackReleases(os_release('keystone'))
|
||||
if cmp_release < 'ocata':
|
||||
return False
|
||||
elif 'ocata' >= cmp_release < 'rocky':
|
||||
return config('token-provider') == 'fernet'
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def fernet_setup():
|
||||
"""Initialize Fernet key repositories"""
|
||||
base_cmd = ['sudo', '-u', 'keystone', 'keystone-manage']
|
||||
subprocess.check_output(base_cmd + ['fernet_setup'])
|
||||
subprocess.check_output(base_cmd + ['credential_setup'])
|
||||
|
||||
|
||||
def fernet_rotate():
|
||||
"""Rotate Fernet keys"""
|
||||
cmd = ['sudo', '-u', 'keystone', 'keystone-manage', 'fernet_rotate']
|
||||
subprocess.check_output(cmd)
|
||||
|
||||
|
||||
def fernet_leader_set():
|
||||
"""Read current key set and update leader storage if necessary"""
|
||||
key_repository = '/etc/keystone/fernet-keys/'
|
||||
disk_keys = []
|
||||
for key in os.listdir(key_repository):
|
||||
with open(os.path.join(key_repository, str(key)), 'r') as f:
|
||||
disk_keys.append(f.read())
|
||||
leader_set({'fernet_keys': json.dumps(disk_keys)})
|
||||
|
||||
|
||||
def fernet_write_keys():
|
||||
"""Get keys from leader storage and write out to disk"""
|
||||
key_repository = '/etc/keystone/fernet-keys/'
|
||||
leader_keys = leader_get('fernet_keys')
|
||||
if not leader_keys:
|
||||
log('"fernet_keys" not in leader settings yet...', level=DEBUG)
|
||||
return
|
||||
mkdir(key_repository, owner=KEYSTONE_USER, group=KEYSTONE_USER,
|
||||
perms=0o700)
|
||||
for idx, key in enumerate(json.loads(leader_keys)):
|
||||
tmp_filename = os.path.join(key_repository, "."+str(idx))
|
||||
key_filename = os.path.join(key_repository, str(idx))
|
||||
# write to tmp file first, move the key into place in an atomic
|
||||
# operation avoiding any races with consumers of the key files
|
||||
write_file(tmp_filename, key, owner=KEYSTONE_USER, group=KEYSTONE_USER,
|
||||
perms=0o600)
|
||||
os.rename(tmp_filename, key_filename)
|
||||
|
|
|
@ -44,8 +44,12 @@ driver = sql
|
|||
[endpoint_filter]
|
||||
|
||||
[token]
|
||||
{% if token_provider == 'fernet' -%}
|
||||
provider = fernet
|
||||
{% else -%}
|
||||
driver = sql
|
||||
provider = uuid
|
||||
{% endif -%}
|
||||
expiration = {{ token_expiration }}
|
||||
|
||||
{% include "parts/section-signing" %}
|
||||
|
|
|
@ -143,6 +143,10 @@ class KeystoneBasicDeployment(OpenStackAmuletDeployment):
|
|||
'admin-password': 'openstack',
|
||||
'admin-token': 'ubuntutesting',
|
||||
'preferred-api-version': self.keystone_api_version,
|
||||
# NOTE(fnordahl): honoured on OpenStack versions Ocata
|
||||
# through Queens, default and not configurable for Rocky,
|
||||
# ignored for all other versions.
|
||||
'token-provider': 'fernet',
|
||||
})
|
||||
|
||||
pxc_config = {
|
||||
|
|
|
@ -89,6 +89,10 @@ TO_PATCH = [
|
|||
'is_db_ready',
|
||||
'create_or_show_domain',
|
||||
'get_api_version',
|
||||
'fernet_enabled',
|
||||
'fernet_leader_set',
|
||||
'fernet_setup',
|
||||
'fernet_write_keys',
|
||||
# other
|
||||
'check_call',
|
||||
'execd_preinstall',
|
||||
|
@ -455,6 +459,7 @@ class KeystoneRelationTests(CharmTestCase):
|
|||
@patch.object(hooks, 'update_all_identity_relation_units')
|
||||
@patch.object(hooks.CONFIGS, 'write')
|
||||
def test_leader_settings_changed(self, mock_write, update):
|
||||
self.os_release.return_value = 'mitaka'
|
||||
self.relation_ids.return_value = ['identity:1']
|
||||
self.related_units.return_value = ['keystone/1']
|
||||
hooks.leader_settings_changed()
|
||||
|
@ -712,6 +717,7 @@ class KeystoneRelationTests(CharmTestCase):
|
|||
self.is_elected_leader.return_value = True
|
||||
is_db_initialized.return_value = False
|
||||
self.is_db_ready.return_value = True
|
||||
self.os_release.return_value = 'mitaka'
|
||||
hooks.leader_init_db_if_ready()
|
||||
self.is_db_ready.assert_called_with(use_current_context=False)
|
||||
self.migrate_database.assert_called_with()
|
||||
|
|
|
@ -12,10 +12,18 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from mock import patch, call, MagicMock
|
||||
from test_utils import CharmTestCase
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from mock import MagicMock, call, mock_open, patch
|
||||
from test_utils import CharmTestCase
|
||||
|
||||
if sys.version_info.major == 2:
|
||||
import __builtin__ as builtins
|
||||
else:
|
||||
import builtins
|
||||
|
||||
os.environ['JUJU_UNIT_NAME'] = 'keystone'
|
||||
with patch('charmhelpers.core.hookenv.config') as config, \
|
||||
|
@ -53,6 +61,8 @@ TO_PATCH = [
|
|||
'related_units',
|
||||
'https',
|
||||
'peer_store',
|
||||
'mkdir',
|
||||
'write_file',
|
||||
# generic
|
||||
'apt_update',
|
||||
'apt_upgrade',
|
||||
|
@ -756,12 +766,6 @@ class TestKeystoneUtils(CharmTestCase):
|
|||
isfile_mock.return_value = False
|
||||
x = utils.get_file_stored_domain_id('/a/file')
|
||||
assert x is None
|
||||
from sys import version_info
|
||||
if version_info.major == 2:
|
||||
import __builtin__ as builtins
|
||||
else:
|
||||
import builtins
|
||||
from mock import mock_open
|
||||
with patch.object(builtins, 'open', mock_open(
|
||||
read_data="some_data\n")):
|
||||
isfile_mock.return_value = True
|
||||
|
@ -1124,3 +1128,80 @@ class TestKeystoneUtils(CharmTestCase):
|
|||
self.get_os_codename_install_source.return_value = 'queens'
|
||||
with self.assertRaises(ValueError):
|
||||
utils.get_api_version()
|
||||
|
||||
def test_fernet_enabled_no_config(self):
|
||||
self.os_release.return_value = 'ocata'
|
||||
self.test_config.set('token-provider', 'uuid')
|
||||
result = utils.fernet_enabled()
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_fernet_enabled_yes_config(self):
|
||||
self.os_release.return_value = 'ocata'
|
||||
self.test_config.set('token-provider', 'fernet')
|
||||
result = utils.fernet_enabled()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_fernet_enabled_no_release_override_config(self):
|
||||
self.os_release.return_value = 'mitaka'
|
||||
self.test_config.set('token-provider', 'fernet')
|
||||
result = utils.fernet_enabled()
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_fernet_enabled_yes_release(self):
|
||||
self.os_release.return_value = 'rocky'
|
||||
result = utils.fernet_enabled()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_fernet_enabled_yes_release_override_config(self):
|
||||
self.os_release.return_value = 'rocky'
|
||||
self.test_config.set('token-provider', 'uuid')
|
||||
result = utils.fernet_enabled()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_fernet_setup(self):
|
||||
base_cmd = ['sudo', '-u', 'keystone', 'keystone-manage']
|
||||
utils.fernet_setup()
|
||||
self.subprocess.check_output.has_calls(
|
||||
[
|
||||
base_cmd + ['fernet_setup'],
|
||||
base_cmd + ['credential_setup'],
|
||||
])
|
||||
|
||||
def test_fernet_rotate(self):
|
||||
cmd = ['sudo', '-u', 'keystone', 'keystone-manage', 'fernet_rotate']
|
||||
utils.fernet_rotate()
|
||||
self.subprocess.check_output.called_with(cmd)
|
||||
|
||||
@patch.object(utils, 'leader_set')
|
||||
@patch('os.listdir')
|
||||
def test_fernet_leader_set(self, listdir, leader_set):
|
||||
listdir.return_value = [0, 1]
|
||||
with patch.object(builtins, 'open', mock_open(
|
||||
read_data="some_data")):
|
||||
utils.fernet_leader_set()
|
||||
listdir.assert_called_with('/etc/keystone/fernet-keys/')
|
||||
leader_set.assert_called_with(
|
||||
{'fernet_keys': json.dumps(['some_data', 'some_data'])})
|
||||
|
||||
@patch('os.rename')
|
||||
@patch.object(utils, 'leader_get')
|
||||
def test_fernet_write_keys(self, leader_get, rename):
|
||||
key_repository = '/etc/keystone/fernet-keys/'
|
||||
leader_get.return_value = json.dumps(['key0', 'key1'])
|
||||
utils.fernet_write_keys()
|
||||
self.mkdir.assert_called_with(key_repository, owner='keystone',
|
||||
group='keystone', perms=0o700)
|
||||
self.write_file.assert_has_calls(
|
||||
[
|
||||
call(os.path.join(key_repository, '.0'), u'key0',
|
||||
owner='keystone', group='keystone', perms=0o600),
|
||||
call(os.path.join(key_repository, '.1'), u'key1',
|
||||
owner='keystone', group='keystone', perms=0o600),
|
||||
])
|
||||
rename.assert_has_calls(
|
||||
[
|
||||
call(os.path.join(key_repository, '.0'),
|
||||
os.path.join(key_repository, '0')),
|
||||
call(os.path.join(key_repository, '.1'),
|
||||
os.path.join(key_repository, '1')),
|
||||
])
|
||||
|
|
Loading…
Reference in New Issue