keystone-manage doctor

This introduces a new keystone-manage command called 'doctor' which
attempts to diagnose and report on various ill-advised configurations
and deployment states.

The number of checks we could perform is basically endless, so this is
just a random sampling of checks to get the ball rolling. The idea is
that as new features are introduced, as default configurations change,
as we have new recommendations to make to deployers, etc, we can
implement new checks in keystone-manage doctor and communicate our
concerns directly to those operated affected deployments.

Change-Id: Ib6660c1a885c439ca03357870628b2ea52e39e9d
Implements: bp keystone-manage-doctor
This commit is contained in:
Dolph Mathews 2016-07-15 21:02:51 +00:00
parent 5ac2029856
commit 059f35302d
9 changed files with 337 additions and 0 deletions

View File

@ -43,6 +43,7 @@ Available commands:
* ``bootstrap``: Perform the basic bootstrap process.
* ``db_sync``: Sync the database.
* ``db_version``: Print the current migration version of the database.
* ``doctor``: Diagnose common problems with keystone deployments.
* ``domain_config_upload``: Upload domain configuration file.
* ``fernet_rotate``: Rotate keys in the Fernet key repository.
* ``fernet_setup``: Setup a Fernet key repository.

View File

@ -25,6 +25,7 @@ from oslo_log import versionutils
from oslo_serialization import jsonutils
import pbr.version
from keystone.cmd import doctor
from keystone.common import driver_hints
from keystone.common import openssl
from keystone.common import sql
@ -361,6 +362,22 @@ class BootStrap(BaseApp):
klass.do_bootstrap()
class Doctor(BaseApp):
"""Diagnose common problems with keystone deployments."""
name = 'doctor'
@classmethod
def add_argument_parser(cls, subparsers):
parser = super(Doctor, cls).add_argument_parser(subparsers)
return parser
@staticmethod
def main():
# Return a non-zero exit code if we detect any symptoms.
raise SystemExit(doctor.diagnose())
class DbSync(BaseApp):
"""Sync the database."""
@ -942,6 +959,7 @@ CMDS = [
BootStrap,
DbSync,
DbVersion,
Doctor,
DomainConfigUpload,
FernetRotate,
FernetSetup,

View File

@ -0,0 +1,76 @@
# 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_log import log
from keystone.cmd.doctor import caching
from keystone.cmd.doctor import database
from keystone.cmd.doctor import federation
from keystone.cmd.doctor import ldap
from keystone.cmd.doctor import tokens
from keystone.cmd.doctor import tokens_fernet
import keystone.conf
from keystone.i18n import _
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
SYMPTOM_PREFIX = 'symptom_'
SYMPTOM_MODULES = [
caching,
database,
federation,
ldap,
tokens,
tokens_fernet]
def diagnose():
"""Report diagnosis for any symptoms we find.
Returns true when any symptoms are found, false otherwise.
"""
symptoms_found = False
for symptom in gather_symptoms():
if CONF.debug:
# Some symptoms may take a long time to check, so let's keep
# curious users posted on our progress as we go.
print(
'Checking for %s...' %
symptom.__name__[len(SYMPTOM_PREFIX):].replace('_', ' '))
# All symptoms are just callables that return true when they match the
# condition that they're looking for. When that happens, we need to
# inform the user by providing some helpful documentation.
if symptom():
# We use this to keep track of our exit condition
symptoms_found = True
# Ignore 'H701: empty localization string' because we're definitely
# passing a string here. Also, we include a line break here to
# visually separate the symptom's description from any other
# checks -- it provides a better user experience.
print(_('\nWARNING: %s') % _(symptom.__doc__)) # noqa: See comment above.
return symptoms_found
def gather_symptoms():
"""Gather all of the objects in this module that are named symptom_*."""
symptoms = []
for module in SYMPTOM_MODULES:
for name in dir(module):
if name.startswith(SYMPTOM_PREFIX):
symptoms.append(getattr(module, name))
return symptoms

View File

@ -0,0 +1,35 @@
# 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 keystone.conf
CONF = keystone.conf.CONF
def symptom_caching_disabled():
"""`keystone.conf [caching] enabled` is not enabled.
Caching greatly improves the performance of keystone, and it is highly
recommended that you enable it.
"""
return not CONF.cache.enabled and not CONF.debug
def symptom_caching_enabled_without_a_backend():
"""Caching is not completely configured.
Although caching is enabled in `keystone.conf [cache] enabled`, the default
backend is still set to the no-op backend. Instead, configure keystone to
point to a real caching backend like memcached.
"""
return CONF.cache.enabled and CONF.cache.backend == 'dogpile.cache.null'

View File

@ -0,0 +1,30 @@
# 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 keystone.conf
CONF = keystone.conf.CONF
def symptom_database_connection_is_not_SQLite():
"""SQLite is not recommended for production deployments.
SQLite does not enforce type checking and has limited support for
migrations, making it unsuitable for use in keystone. Please change your
`keystone.conf [database] connection` value to point to a supported
database driver, such as MySQL.
"""
return (
CONF.database.connection is not None
and 'sqlite' in CONF.database.connection
and not CONF.debug)

View File

@ -0,0 +1,36 @@
# 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 keystone.conf
CONF = keystone.conf.CONF
def symptom_comma_in_SAML_public_certificate_path():
"""`[saml] certfile` should not contain a comma (`,`).
Because a comma is part of the API between keystone and the external
xmlsec1 binary which utilizes the certificate, keystone cannot include a
comma in the path to the public certificate file.
"""
return ',' in CONF.saml.certfile
def symptom_comma_in_SAML_private_key_file_path():
"""`[saml] certfile` should not contain a comma (`,`).
Because a comma is part of the API between keystone and the external
xmlsec1 binary which utilizes the key, keystone cannot include a comma in
the path to the private key file.
"""
return ',' in CONF.saml.keyfile

View File

@ -0,0 +1,52 @@
# 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 keystone.conf
CONF = keystone.conf.CONF
def symptom_LDAP_user_enabled_emulation_dn_ignored():
"""`[ldap] user_enabled_emulation_dn` is being ignored.
There is no reason to set this value unless `keystone.conf [ldap]
user_enabled_emulation` is also enabled.
"""
return (
not CONF.ldap.user_enabled_emulation
and CONF.ldap.user_enabled_emulation_dn is not None)
def symptom_LDAP_user_enabled_emulation_use_group_config_ignored():
"""`[ldap] user_enabled_emulation_use_group_config` is being ignored.
There is no reason to set this value unless `keystone.conf [ldap]
user_enabled_emulation` is also enabled.
"""
return (
not CONF.ldap.user_enabled_emulation
and CONF.ldap.user_enabled_emulation_use_group_config)
def symptom_LDAP_group_members_are_ids_disabled():
"""`[ldap] group_members_are_ids` is not enabled.
Because you've set `keystone.conf [ldap] group_objectclass = posixGroup`,
we would have also expected you to enable set `keystone.conf [ldap]
group_members_are_ids` because we suspect you're using Open Directory,
which would contain user ID's in a `posixGroup` rather than LDAP DNs, as
other object classes typically would.
"""
return (
CONF.ldap.group_objectclass == 'posixGroup'
and not CONF.ldap.group_members_are_ids)

View File

@ -0,0 +1,46 @@
# 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 keystone.conf
CONF = keystone.conf.CONF
def symptom_unreasonable_max_token_size():
"""`keystone.conf [DEFAULT] max_token_size` should be adjusted.
This option is intended to protect keystone from unreasonably sized tokens,
where "reasonable" is mostly dependent on the `keystone.conf [token]
provider` that you're using. If you're using one of the following token
providers, then you should set `keystone.conf [DEFAULT] max_token_size`
accordingly:
- For UUID, set `keystone.conf [DEFAULT] max_token_size = 32`, because UUID
tokens are always exactly 32 characters.
- For PKI and PKIZ, set `keystone.conf [DEFAULT] max_token_size = 8192`,
because PKI and PKIZ tokens can be quite large, but any larger than 8192
and they tend to break certain implementations of HTTP.
- For Fernet, set `keystone.conf [DEFAULT] max_token_size = 255`, because
Fernet tokens should never exceed this length in most deployments.
However, if you are also using `keystone.conf [identity] driver = ldap`,
Fernet tokens may not be built using an efficient packing method,
depending on the IDs returned from LDAP, resulting in longer Fernet
tokens (adjust your `max_token_size` accordingly).
"""
return (
'uuid' in CONF.token.provider and CONF.max_token_size != 32
or 'pki' in CONF.token.provider and CONF.max_token_size < 8192
or 'pkiz' in CONF.token.provider and CONF.max_token_size < 8192
or 'fernet' in CONF.token.provider and CONF.max_token_size > 255)

View File

@ -0,0 +1,43 @@
# 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 keystone.conf
from keystone.token.providers.fernet import utils as fernet_utils
CONF = keystone.conf.CONF
def symptom_usability_of_Fernet_key_repository():
"""Fernet key repository is not setup correctly.
The Fernet key repository is expected to be readable by the user running
keystone, but not world-readable, because it contains security-sensitive
secrets.
"""
return (
'fernet' in CONF.token.provider
and not fernet_utils.validate_key_repository())
def symptom_keys_in_Fernet_key_repository():
"""Fernet key repository is empty.
After configuring keystone to use the Fernet token provider, you should use
`keystone-manage fernet_setup` to initially populate your key repository
with keys, and periodically rotate your keys with `keystone-manage
fernet_rotate`.
"""
return (
'fernet' in CONF.token.provider
and not fernet_utils.load_keys())