Skip to content

Commit

Permalink
Merge "Implement auth receipts spec"
Browse files Browse the repository at this point in the history
  • Loading branch information
Zuul authored and openstack-gerrit committed Nov 2, 2018
2 parents eda65d7 + d9e6c1d commit c785729
Show file tree
Hide file tree
Showing 27 changed files with 2,032 additions and 10 deletions.
13 changes: 11 additions & 2 deletions keystone/api/_shared/authentication.py
Expand Up @@ -26,6 +26,7 @@
from keystone import exception
from keystone.federation import constants
from keystone.i18n import _
from keystone.receipt import handlers as receipt_handlers


LOG = log.getLogger(__name__)
Expand Down Expand Up @@ -191,11 +192,19 @@ def authenticate_for_token(auth=None):
)
trust_id = trust.get('id') if trust else None

receipt = receipt_handlers.extract_receipt(auth_context)

# NOTE(notmorgan): only methods that actually run and succeed will
# be in the auth_context['method_names'] list. Do not blindly take
# the values from auth_info, look at the authoritative values. Make
# sure the set is unique.
method_names_set = set(auth_context.get('method_names', []))
# NOTE(adriant): The set of methods will also include any methods from
# the given receipt.
if receipt:
method_names_set = set(
auth_context.get('method_names', []) + receipt.methods)
else:
method_names_set = set(auth_context.get('method_names', []))
method_names = list(method_names_set)

app_cred_id = None
Expand All @@ -208,7 +217,7 @@ def authenticate_for_token(auth=None):
auth_context['user_id'], method_names_set):
raise exception.InsufficientAuthMethods(
user_id=auth_context['user_id'],
methods='[%s]' % ','.join(auth_info.get_method_names()))
methods=method_names)

expires_at = auth_context.get('expires_at')
token_audit_id = auth_context.get('audit_id')
Expand Down
102 changes: 99 additions & 3 deletions keystone/cmd/cli.py
Expand Up @@ -385,11 +385,11 @@ def get_user_group():


class FernetSetup(BasePermissionsSetup):
"""Setup a key repository for Fernet tokens.
"""Setup key repositories for Fernet tokens and auth receipts.
This also creates a primary key used for both creating and validating
Fernet tokens. To improve security, you should rotate your keys (using
keystone-manage fernet_rotate, for example).
Fernet tokens and auth receipts. To improve security, you should rotate
your keys (using keystone-manage fernet_rotate, for example).
"""

Expand All @@ -409,6 +409,32 @@ def main(cls):
futils.initialize_key_repository(
keystone_user_id, keystone_group_id)

if (CONF.fernet_tokens.key_repository !=
CONF.fernet_receipts.key_repository):
futils = fernet_utils.FernetUtils(
CONF.fernet_receipts.key_repository,
CONF.fernet_receipts.max_active_keys,
'fernet_receipts'
)

futils.create_key_directory(keystone_user_id, keystone_group_id)
if futils.validate_key_repository(requires_write=True):
futils.initialize_key_repository(
keystone_user_id, keystone_group_id)
elif(CONF.fernet_tokens.max_active_keys !=
CONF.fernet_receipts.max_active_keys):
# WARNING(adriant): If the directories are the same,
# 'max_active_keys' is ignored from fernet_receipts in favor of
# fernet_tokens to avoid a potential mismatch. Only if the
# directories are different do we create a different one for
# receipts, and then respect 'max_active_keys' for receipts.
LOG.warning(
"Receipt and Token fernet key directories are the same "
"but `max_active_keys` is different. Receipt "
"`max_active_keys` will be ignored in favor of Token "
"`max_active_keys`."
)


class FernetRotate(BasePermissionsSetup):
"""Rotate Fernet encryption keys.
Expand Down Expand Up @@ -442,6 +468,17 @@ def main(cls):
if futils.validate_key_repository(requires_write=True):
futils.rotate_keys(keystone_user_id, keystone_group_id)

if (CONF.fernet_tokens.key_repository !=
CONF.fernet_receipts.key_repository):
futils = fernet_utils.FernetUtils(
CONF.fernet_receipts.key_repository,
CONF.fernet_receipts.max_active_keys,
'fernet_receipts'
)

if futils.validate_key_repository(requires_write=True):
futils.rotate_keys(keystone_user_id, keystone_group_id)


class TokenSetup(BasePermissionsSetup):
"""Setup a key repository for tokens.
Expand Down Expand Up @@ -504,6 +541,65 @@ def main(cls):
futils.rotate_keys(keystone_user_id, keystone_group_id)


class ReceiptSetup(BasePermissionsSetup):
"""Setup a key repository for auth receipts.
This also creates a primary key used for both creating and validating
receipts. To improve security, you should rotate your keys (using
keystone-manage receipt_rotate, for example).
"""

name = 'receipt_setup'

@classmethod
def main(cls):
futils = fernet_utils.FernetUtils(
CONF.fernet_receipts.key_repository,
CONF.fernet_receipts.max_active_keys,
'fernet_receipts'
)

keystone_user_id, keystone_group_id = cls.get_user_group()
futils.create_key_directory(keystone_user_id, keystone_group_id)
if futils.validate_key_repository(requires_write=True):
futils.initialize_key_repository(
keystone_user_id, keystone_group_id)


class ReceiptRotate(BasePermissionsSetup):
"""Rotate auth receipts encryption keys.
This assumes you have already run keystone-manage receipt_setup.
A new primary key is placed into rotation, which is used for new receipts.
The old primary key is demoted to secondary, which can then still be used
for validating receipts. Excess secondary keys (beyond [receipt]
max_active_keys) are revoked. Revoked keys are permanently deleted. A new
staged key will be created and used to validate receipts. The next time key
rotation takes place, the staged key will be put into rotation as the
primary key.
Rotating keys too frequently, or with [receipt] max_active_keys set
too low, will cause receipts to become invalid prior to their expiration.
"""

name = 'receipt_rotate'

@classmethod
def main(cls):
futils = fernet_utils.FernetUtils(
CONF.fernet_receipts.key_repository,
CONF.fernet_receipts.max_active_keys,
'fernet_receipts'
)

keystone_user_id, keystone_group_id = cls.get_user_group()
if futils.validate_key_repository(requires_write=True):
futils.rotate_keys(keystone_user_id, keystone_group_id)


class CredentialSetup(BasePermissionsSetup):
"""Setup a Fernet key repository for credential encryption.
Expand Down
5 changes: 5 additions & 0 deletions keystone/common/authorization.py
Expand Up @@ -21,6 +21,11 @@
# Header used to transmit the auth token
AUTH_TOKEN_HEADER = 'X-Auth-Token'


# Header used to transmit the auth receipt
AUTH_RECEIPT_HEADER = 'Openstack-Auth-Receipt'


# Header used to transmit the subject token
SUBJECT_TOKEN_HEADER = 'X-Subject-Token'

Expand Down
10 changes: 8 additions & 2 deletions keystone/conf/__init__.py
Expand Up @@ -31,13 +31,15 @@
from keystone.conf import endpoint_policy
from keystone.conf import eventlet_server
from keystone.conf import federation
from keystone.conf import fernet_receipts
from keystone.conf import fernet_tokens
from keystone.conf import identity
from keystone.conf import identity_mapping
from keystone.conf import ldap
from keystone.conf import memcache
from keystone.conf import oauth1
from keystone.conf import policy
from keystone.conf import receipt
from keystone.conf import resource
from keystone.conf import revoke
from keystone.conf import role
Expand Down Expand Up @@ -66,13 +68,15 @@
endpoint_policy,
eventlet_server,
federation,
fernet_receipts,
fernet_tokens,
identity,
identity_mapping,
ldap,
memcache,
oauth1,
policy,
receipt,
resource,
revoke,
role,
Expand Down Expand Up @@ -163,10 +167,12 @@ def set_external_opts_defaults():
'X-Project-Domain-Id',
'X-Project-Domain-Name',
'X-Domain-Id',
'X-Domain-Name'],
'X-Domain-Name',
'Openstack-Auth-Receipt'],
expose_headers=['X-Auth-Token',
'X-Openstack-Request-Id',
'X-Subject-Token'],
'X-Subject-Token',
'Openstack-Auth-Receipt'],
allow_methods=['GET',
'PUT',
'POST',
Expand Down
71 changes: 71 additions & 0 deletions keystone/conf/fernet_receipts.py
@@ -0,0 +1,71 @@
# Copyright 2018 Catalyst Cloud Ltd
#
# 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.

from oslo_config import cfg

from keystone.conf import utils


key_repository = cfg.StrOpt(
'key_repository',
default='/etc/keystone/fernet-keys/',
help=utils.fmt("""
Directory containing Fernet receipt keys. This directory must exist before
using `keystone-manage fernet_setup` for the first time, must be writable by
the user running `keystone-manage fernet_setup` or `keystone-manage
fernet_rotate`, and of course must be readable by keystone's server process.
The repository may contain keys in one of three states: a single staged key
(always index 0) used for receipt validation, a single primary key (always the
highest index) used for receipt creation and validation, and any number of
secondary keys (all other index values) used for receipt validation. With
multiple keystone nodes, each node must share the same key repository contents,
with the exception of the staged key (index 0). It is safe to run
`keystone-manage fernet_rotate` once on any one node to promote a staged key
(index 0) to be the new primary (incremented from the previous highest index),
and produce a new staged key (a new key with index 0); the resulting repository
can then be atomically replicated to other nodes without any risk of race
conditions (for example, it is safe to run `keystone-manage fernet_rotate` on
host A, wait any amount of time, create a tarball of the directory on host A,
unpack it on host B to a temporary location, and atomically move (`mv`) the
directory into place on host B). Running `keystone-manage fernet_rotate`
*twice* on a key repository without syncing other nodes will result in receipts
that can not be validated by all nodes.
"""))

max_active_keys = cfg.IntOpt(
'max_active_keys',
default=3,
min=1,
help=utils.fmt("""
This controls how many keys are held in rotation by `keystone-manage
fernet_rotate` before they are discarded. The default value of 3 means that
keystone will maintain one staged key (always index 0), one primary key (the
highest numerical index), and one secondary key (every other index). Increasing
this value means that additional secondary keys will be kept in the rotation.
"""))


GROUP_NAME = __name__.split('.')[-1]
ALL_OPTS = [
key_repository,
max_active_keys,
]


def register_opts(conf):
conf.register_opts(ALL_OPTS, group=GROUP_NAME)


def list_opts():
return {GROUP_NAME: ALL_OPTS}
86 changes: 86 additions & 0 deletions keystone/conf/receipt.py
@@ -0,0 +1,86 @@
# Copyright 2018 Catalyst Cloud Ltd
#
# 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.

from oslo_config import cfg

from keystone.conf import utils


expiration = cfg.IntOpt(
'expiration',
default=300,
min=0,
max=86400,
help=utils.fmt("""
The amount of time that a receipt should remain valid (in seconds). This value
should always be very short, as it represents how long a user has to reattempt
auth with the missing auth methods.
"""))

provider = cfg.StrOpt(
'provider',
default='fernet',
help=utils.fmt("""
Entry point for the receipt provider in the `keystone.receipt.provider`
namespace. The receipt provider controls the receipt construction and
validation operations. Keystone includes just the `fernet` receipt provider for
now. `fernet` receipts do not need to be persisted at all, but require that you
run `keystone-manage fernet_setup` (also see the `keystone-manage
fernet_rotate` command).
"""))

caching = cfg.BoolOpt(
'caching',
default=True,
help=utils.fmt("""
Toggle for caching receipt creation and validation data. This has no effect
unless global caching is enabled, or if cache_on_issue is disabled as we only
cache receipts on issue.
"""))

cache_time = cfg.IntOpt(
'cache_time',
default=300,
min=0,
help=utils.fmt("""
The number of seconds to cache receipt creation and validation data. This has
no effect unless both global and `[receipt] caching` are enabled.
"""))

cache_on_issue = cfg.BoolOpt(
'cache_on_issue',
default=True,
help=utils.fmt("""
Enable storing issued receipt data to receipt validation cache so that first
receipt validation doesn't actually cause full validation cycle. This option
has no effect unless global caching and receipt caching are enabled.
"""))


GROUP_NAME = __name__.split('.')[-1]
ALL_OPTS = [
expiration,
provider,
caching,
cache_time,
cache_on_issue,
]


def register_opts(conf):
conf.register_opts(ALL_OPTS, group=GROUP_NAME)


def list_opts():
return {GROUP_NAME: ALL_OPTS}

0 comments on commit c785729

Please sign in to comment.