Removed the deprecated pki_setup command

bp removed-as-of-pike

Change-Id: Ib39d21ed547e3be7a3a2c333a7193f990043a80b
This commit is contained in:
David Stanek 2017-02-08 13:45:17 +00:00
parent 30faacc5d0
commit 928d23db02
7 changed files with 32 additions and 571 deletions

View File

@ -949,29 +949,6 @@ If your certificate directory path is different from the default
section of the configuration file.
Generating a Signing Certificate using ``pki_setup``
----------------------------------------------------
``keystone-manage pki_setup`` is a development tool. We recommend that you do
not use ``keystone-manage pki_setup`` in a production environment. In
production, an external CA should be used instead. This is because the CA
secret key should generally be kept apart from the token signing secret keys so
that a compromise of a node does not lead to an attacker being able to generate
valid signed keystone tokens. This is a low probability attack vector, as
compromise of a keystone service machine's filesystem security almost certainly
means the attacker will be able to gain direct access to the token backend.
When using the ``keystone-manage pki_setup`` to generate the certificates, the
following configuration options in the ``[signing]`` section are used:
* ``ca_key`` - Default is ``/etc/keystone/ssl/private/cakey.pem``
* ``key_size`` - Default is ``2048``
* ``valid_days`` - Default is ``3650``
If ``keystone-manage pki_setup`` is not used then these options don't need to
be set.
Service Catalog
===============

View File

@ -22,6 +22,5 @@ Available commands:
* ``mapping_populate``: Prepare domain-specific LDAP backend.
* ``mapping_purge``: Purge the identity mapping table.
* ``mapping_engine``: Test your federation mapping rules.
* ``pki_setup``: Initialize the certificates used to sign revocation lists. **deprecated**
* ``saml_idp_metadata``: Generate identity provider metadata.
* ``token_flush``: Purge expired tokens.

View File

@ -30,7 +30,6 @@ import pbr.version
from keystone.cmd import doctor
from keystone.common import driver_hints
from keystone.common import fernet_utils
from keystone.common import openssl
from keystone.common import sql
from keystone.common.sql import upgrades
from keystone.common import utils
@ -582,44 +581,6 @@ class BasePermissionsSetup(BaseApp):
return keystone_user_id, keystone_group_id
class BaseCertificateSetup(BasePermissionsSetup):
"""Provides common options for certificate setup."""
@classmethod
def add_argument_parser(cls, subparsers):
parser = super(BaseCertificateSetup,
cls).add_argument_parser(subparsers)
parser.add_argument('--rebuild', default=False, action='store_true',
help=('Rebuild certificate files: erase previous '
'files and regenerate them.'))
return parser
class PKISetup(BaseCertificateSetup):
"""Setup keys and certificates for signing and verifying revocation lists.
This is NOT intended for production use, see Keystone Configuration
documentation for details. As of the Mitaka release, this command has
been DEPRECATED and may be removed in the 'O' release.
"""
name = 'pki_setup'
@classmethod
def main(cls):
versionutils.report_deprecated_feature(
LOG,
"keystone-manage pki_setup is deprecated as of Mitaka in "
"favor of not using PKI tokens and may be removed in 'O' "
"release.")
LOG.warning('keystone-manage pki_setup is not recommended for '
'production use.')
keystone_user_id, keystone_group_id = cls.get_user_group()
conf_pki = openssl.ConfigurePKI(keystone_user_id, keystone_group_id,
rebuild=CONF.command.rebuild)
conf_pki.run()
class FernetSetup(BasePermissionsSetup):
"""Setup a key repository for Fernet tokens.
@ -1336,7 +1297,6 @@ CMDS = [
MappingPopulate,
MappingPurge,
MappingEngineTester,
PKISetup,
SamlIdentityProviderMetadata,
TokenFlush,
]

View File

@ -1,324 +0,0 @@
# Copyright 2012 OpenStack Foundation
#
# 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 os
import subprocess # nosec : see comments in the code below
from oslo_log import log
from keystone.common import utils
import keystone.conf
LOG = log.getLogger(__name__)
CONF = keystone.conf.CONF
PUBLIC_DIR_PERMS = 0o755 # -rwxr-xr-x
PRIVATE_DIR_PERMS = 0o750 # -rwxr-x---
PUBLIC_FILE_PERMS = 0o644 # -rw-r--r--
PRIVATE_FILE_PERMS = 0o640 # -rw-r-----
def file_exists(file_path):
return os.path.exists(file_path)
class BaseCertificateConfigure(object):
"""Create a certificate signing environment.
This is based on a config section and reasonable OpenSSL defaults.
"""
def __init__(self, conf_obj, keystone_user,
keystone_group, rebuild, **kwargs):
self.conf_dir = os.path.dirname(conf_obj.ca_certs)
self.use_keystone_user = keystone_user
self.use_keystone_group = keystone_group
self.rebuild = rebuild
self.ssl_config_file_name = os.path.join(self.conf_dir, "openssl.conf")
self.request_file_name = os.path.join(self.conf_dir, "req.pem")
self.ssl_dictionary = {'conf_dir': self.conf_dir,
'ca_cert': conf_obj.ca_certs,
'default_md': 'default',
'ssl_config': self.ssl_config_file_name,
'ca_private_key': conf_obj.ca_key,
'request_file': self.request_file_name,
'signing_key': conf_obj.keyfile,
'signing_cert': conf_obj.certfile,
'key_size': int(conf_obj.key_size),
'valid_days': int(conf_obj.valid_days),
'cert_subject': conf_obj.cert_subject}
try:
# OpenSSL 1.0 and newer support default_md = default,
# older versions do not
openssl_ver = subprocess.check_output( # nosec : the arguments
# are hardcoded and just check the openssl version
['openssl', 'version'])
if b'OpenSSL 0.' in openssl_ver:
self.ssl_dictionary['default_md'] = 'sha1'
except subprocess.CalledProcessError:
LOG.warning('Failed to invoke ``openssl version``, '
'assuming is v1.0 or newer')
self.ssl_dictionary.update(kwargs)
def exec_command(self, command):
to_exec = [part % self.ssl_dictionary for part in command]
LOG.info('Running command - %s', ' '.join(to_exec))
try:
# NOTE(shaleh): use check_output instead of the simpler
# `check_call()` in order to log any output from an error.
subprocess.check_output( # nosec : the arguments being passed
# in are defined in this file and trusted to build CAs, keys
# and certs
to_exec,
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
msg = ("Command %(to_exec)s exited with %(retcode)s - "
"%(output)s)")
LOG.error(msg,
{'to_exec': to_exec,
'retcode': e.returncode,
'output': e.output})
raise
def clean_up_existing_files(self):
files_to_clean = [self.ssl_dictionary['ca_private_key'],
self.ssl_dictionary['ca_cert'],
self.ssl_dictionary['signing_key'],
self.ssl_dictionary['signing_cert'],
]
existing_files = []
for file_path in files_to_clean:
if file_exists(file_path):
if self.rebuild:
# The file exists but the user wants to rebuild it, so blow
# it away
try:
os.remove(file_path)
except OSError as exc:
msg = ("Failed to remove file %(file_path)r: "
"%(error)s")
LOG.error(msg,
{'file_path': file_path,
'error': exc.strerror})
raise
else:
existing_files.append(file_path)
return existing_files
def build_ssl_config_file(self):
utils.make_dirs(os.path.dirname(self.ssl_config_file_name),
mode=PUBLIC_DIR_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
if not file_exists(self.ssl_config_file_name):
with open(self.ssl_config_file_name, 'w') as ssl_config_file:
ssl_config_file.write(self.sslconfig % self.ssl_dictionary)
utils.set_permissions(self.ssl_config_file_name,
mode=PRIVATE_FILE_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
index_file_name = os.path.join(self.conf_dir, 'index.txt')
if not file_exists(index_file_name):
with open(index_file_name, 'w') as index_file:
index_file.write('')
utils.set_permissions(index_file_name,
mode=PRIVATE_FILE_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
serial_file_name = os.path.join(self.conf_dir, 'serial')
if not file_exists(serial_file_name):
with open(serial_file_name, 'w') as index_file:
index_file.write('01')
utils.set_permissions(serial_file_name,
mode=PRIVATE_FILE_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
def build_ca_cert(self):
ca_key_file = self.ssl_dictionary['ca_private_key']
utils.make_dirs(os.path.dirname(ca_key_file),
mode=PRIVATE_DIR_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
if not file_exists(ca_key_file):
self.exec_command(['openssl', 'genrsa',
'-out', '%(ca_private_key)s',
'%(key_size)d'])
utils.set_permissions(ca_key_file,
mode=PRIVATE_FILE_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
ca_cert = self.ssl_dictionary['ca_cert']
utils.make_dirs(os.path.dirname(ca_cert),
mode=PUBLIC_DIR_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
if not file_exists(ca_cert):
self.exec_command(['openssl', 'req', '-new', '-x509',
'-extensions', 'v3_ca',
'-key', '%(ca_private_key)s',
'-out', '%(ca_cert)s',
'-days', '%(valid_days)d',
'-config', '%(ssl_config)s',
'-subj', '%(cert_subject)s'])
utils.set_permissions(ca_cert,
mode=PUBLIC_FILE_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
def build_private_key(self):
signing_keyfile = self.ssl_dictionary['signing_key']
utils.make_dirs(os.path.dirname(signing_keyfile),
mode=PRIVATE_DIR_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
if not file_exists(signing_keyfile):
self.exec_command(['openssl', 'genrsa', '-out', '%(signing_key)s',
'%(key_size)d'])
utils.set_permissions(signing_keyfile,
mode=PRIVATE_FILE_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
def build_signing_cert(self):
signing_cert = self.ssl_dictionary['signing_cert']
utils.make_dirs(os.path.dirname(signing_cert),
mode=PUBLIC_DIR_PERMS,
user=self.use_keystone_user,
group=self.use_keystone_group, log=LOG)
if not file_exists(signing_cert):
self.exec_command(['openssl', 'req', '-key', '%(signing_key)s',
'-new', '-out', '%(request_file)s',
'-config', '%(ssl_config)s',
'-subj', '%(cert_subject)s'])
self.exec_command(['openssl', 'ca', '-batch',
'-out', '%(signing_cert)s',
'-config', '%(ssl_config)s',
'-days', '%(valid_days)d',
'-cert', '%(ca_cert)s',
'-keyfile', '%(ca_private_key)s',
'-infiles', '%(request_file)s'])
def run(self):
try:
existing_files = self.clean_up_existing_files()
except OSError:
print('An error occurred when rebuilding cert files.')
return
if existing_files:
print('The following cert files already exist, use --rebuild to '
'remove the existing files before regenerating:')
for f in existing_files:
print('%s already exists' % f)
return
self.build_ssl_config_file()
self.build_ca_cert()
self.build_private_key()
self.build_signing_cert()
class ConfigurePKI(BaseCertificateConfigure):
"""Generate files for PKI signing using OpenSSL.
Signed tokens require a private key and signing certificate which itself
must be signed by a CA. This class generates them with workable defaults
if each of the files are not present
"""
def __init__(self, keystone_user, keystone_group, rebuild=False):
super(ConfigurePKI, self).__init__(CONF.signing, keystone_user,
keystone_group, rebuild=rebuild)
BaseCertificateConfigure.sslconfig = """
# OpenSSL configuration file.
#
# Establish working directory.
dir = %(conf_dir)s
[ ca ]
default_ca = CA_default
[ CA_default ]
new_certs_dir = $dir
serial = $dir/serial
database = $dir/index.txt
default_days = 365
default_md = %(default_md)s
preserve = no
email_in_dn = no
nameopt = default_ca
certopt = default_ca
policy = policy_anything
x509_extensions = usr_cert
unique_subject = no
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 2048 # Size of keys
default_keyfile = key.pem # name of generated keys
string_mask = utf8only # permitted characters
distinguished_name = req_distinguished_name
req_extensions = v3_req
x509_extensions = v3_ca
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
localityName = Locality Name (city, district)
0.organizationName = Organization Name (company)
organizationalUnitName = Organizational Unit Name (department, division)
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
[ v3_ca ]
basicConstraints = CA:TRUE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
[ usr_cert ]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
"""

View File

@ -11,14 +11,23 @@
# under the License.
from oslo_config import cfg
from oslo_log import versionutils
from keystone.conf import constants
from keystone.conf import utils
_DEPRECATED_MSG = utils.fmt("""
`keystone-manage pki_setup` was deprecated in Mitaka and removed in Pike.
These options remain for backwards compatibility.
""")
certfile = cfg.StrOpt(
'certfile',
default=constants._CERTFILE,
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
help=utils.fmt("""
Absolute path to the public certificate file to use for signing responses to
revocation lists requests. Set this together with `[signing] keyfile`. For
@ -29,6 +38,9 @@ pki_setup` to generate self-signed certificates.
keyfile = cfg.StrOpt(
'keyfile',
default=constants._KEYFILE,
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
help=utils.fmt("""
Absolute path to the private key file to use for signing responses to
revocation lists requests. Set this together with `[signing] certfile`.
@ -36,6 +48,9 @@ revocation lists requests. Set this together with `[signing] certfile`.
ca_certs = cfg.StrOpt(
'ca_certs',
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
default='/etc/keystone/ssl/certs/ca.pem',
help=utils.fmt("""
Absolute path to the public certificate authority (CA) file to use when
@ -48,6 +63,9 @@ you are requesting revocation lists in a non-production environment. Use a
ca_key = cfg.StrOpt(
'ca_key',
default='/etc/keystone/ssl/private/cakey.pem',
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
help=utils.fmt("""
Absolute path to the private certificate authority (CA) key file to use when
creating self-signed certificates with `keystone-manage pki_setup`. Set this
@ -60,6 +78,9 @@ key_size = cfg.IntOpt(
'key_size',
default=2048,
min=1024,
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
help=utils.fmt("""
Key size (in bits) to use when generating a self-signed token signing
certificate. There is no reason to set this option unless you are requesting
@ -70,6 +91,9 @@ issued from a trusted certificate authority instead.
valid_days = cfg.IntOpt(
'valid_days',
default=3650,
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
help=utils.fmt("""
The validity period (in days) to use when generating a self-signed token
signing certificate. There is no reason to set this option unless you are
@ -80,6 +104,9 @@ requesting revocation lists in a non-production environment. Use a
cert_subject = cfg.StrOpt(
'cert_subject',
default=('/C=US/ST=Unset/L=Unset/O=Unset/CN=www.example.com'),
deprecated_for_removal=True,
deprecated_reason=_DEPRECATED_MSG,
deprecated_since=versionutils.deprecated.PIKE,
help=utils.fmt("""
The certificate subject to use when generating a self-signed token signing
certificate. There is no reason to set this option unless you are requesting

View File

@ -1,183 +0,0 @@
# Copyright 2012 OpenStack Foundation
#
# 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 os
import shutil
import subprocess
import mock
from six.moves import http_client
from testtools import matchers
from keystone.common import openssl
from keystone.tests import unit
from keystone.tests.unit import ksfixtures
from keystone.tests.unit import rest
SSLDIR = unit.dirs.tmp('ssl')
CONF = unit.CONF
CERTDIR = os.path.join(SSLDIR, 'certs')
KEYDIR = os.path.join(SSLDIR, 'private')
class CertSetupTestCase(rest.RestfulTestCase):
def setUp(self):
super(CertSetupTestCase, self).setUp()
def cleanup_ssldir():
try:
shutil.rmtree(SSLDIR)
except OSError:
pass
self.addCleanup(cleanup_ssldir)
def config_overrides(self):
super(CertSetupTestCase, self).config_overrides()
ca_certs = os.path.join(CERTDIR, 'ca.pem')
ca_key = os.path.join(CERTDIR, 'cakey.pem')
self.config_fixture.config(
group='signing',
certfile=os.path.join(CERTDIR, 'signing_cert.pem'),
ca_certs=ca_certs,
ca_key=ca_key,
keyfile=os.path.join(KEYDIR, 'signing_key.pem'))
self.config_fixture.config(group='token', provider='fernet')
self.useFixture(
ksfixtures.KeyRepository(
self.config_fixture,
'fernet_tokens',
CONF.fernet_tokens.max_active_keys
)
)
def test_create_pki_certs(self, rebuild=False):
pki = openssl.ConfigurePKI(None, None, rebuild=rebuild)
pki.run()
self.assertTrue(os.path.exists(CONF.signing.certfile))
self.assertTrue(os.path.exists(CONF.signing.ca_certs))
self.assertTrue(os.path.exists(CONF.signing.keyfile))
def test_fetch_signing_cert(self, rebuild=False):
pki = openssl.ConfigurePKI(None, None, rebuild=rebuild)
pki.run()
# NOTE(jamielennox): Use request directly because certificate
# requests don't have some of the normal information
signing_resp = self.request(self.public_app,
'/v2.0/certificates/signing',
method='GET',
expected_status=http_client.OK)
cacert_resp = self.request(self.public_app,
'/v2.0/certificates/ca',
method='GET',
expected_status=http_client.OK)
with open(CONF.signing.certfile) as f:
self.assertEqual(f.read(), signing_resp.text)
with open(CONF.signing.ca_certs) as f:
self.assertEqual(f.read(), cacert_resp.text)
# NOTE(jamielennox): This is weird behaviour that we need to enforce.
# It doesn't matter what you ask for it's always going to give text
# with a text/html content_type.
for path in ['/v2.0/certificates/signing', '/v2.0/certificates/ca']:
for accept in [None, 'text/html', 'application/json', 'text/xml']:
headers = {'Accept': accept} if accept else {}
resp = self.request(self.public_app, path, method='GET',
expected_status=http_client.OK,
headers=headers)
self.assertEqual('text/html', resp.content_type)
def test_fetch_signing_cert_when_rebuild(self):
pki = openssl.ConfigurePKI(None, None)
pki.run()
self.test_fetch_signing_cert(rebuild=True)
def test_failure(self):
for path in ['/v2.0/certificates/signing', '/v2.0/certificates/ca']:
self.request(self.public_app, path, method='GET',
expected_status=http_client.INTERNAL_SERVER_ERROR)
def test_pki_certs_rebuild(self):
self.test_create_pki_certs()
with open(CONF.signing.certfile) as f:
cert_file1 = f.read()
self.test_create_pki_certs(rebuild=True)
with open(CONF.signing.certfile) as f:
cert_file2 = f.read()
self.assertNotEqual(cert_file1, cert_file2)
@mock.patch.object(os, 'remove')
def test_rebuild_pki_certs_remove_error(self, mock_remove):
self.test_create_pki_certs()
with open(CONF.signing.certfile) as f:
cert_file1 = f.read()
mock_remove.side_effect = OSError()
self.test_create_pki_certs(rebuild=True)
with open(CONF.signing.certfile) as f:
cert_file2 = f.read()
self.assertEqual(cert_file1, cert_file2)
def test_create_pki_certs_twice_without_rebuild(self):
self.test_create_pki_certs()
with open(CONF.signing.certfile) as f:
cert_file1 = f.read()
self.test_create_pki_certs()
with open(CONF.signing.certfile) as f:
cert_file2 = f.read()
self.assertEqual(cert_file1, cert_file2)
class TestExecCommand(unit.TestCase):
@mock.patch.object(subprocess.Popen, 'poll')
def test_running_a_successful_command(self, mock_poll):
mock_poll.return_value = 0
ssl = openssl.ConfigurePKI('keystone_user', 'keystone_group')
ssl.exec_command(['ls'])
@mock.patch.object(subprocess, 'check_output')
def test_running_an_invalid_command(self, mock_check_output):
cmd = ['ls']
output = 'this is the output string'
error = subprocess.CalledProcessError(returncode=1,
cmd=cmd,
output=output)
mock_check_output.side_effect = error
ssl = openssl.ConfigurePKI('keystone_user', 'keystone_group')
e = self.assertRaises(subprocess.CalledProcessError,
ssl.exec_command,
cmd)
self.assertThat(e.output, matchers.Equals(output))

View File

@ -31,3 +31,8 @@ other:
The ``keystone.common.ldap`` module was removed from the code tree. It was
deprecated in the Newton release in favor of using
``keystone.identity.backends.ldap.common`` which has the same functionality.
- >
[`blueprint removed-as-of-pike <https://blueprints.launchpad.net/keystone/+spec/removed-as-of-pike>`_]
The ``keystone-manage pki_setup`` was added to aid developer setup by hiding the sometimes cryptic
openssl commands. This is no longer needed since keystone no longer supports PKI tokens and can no
longer serve SSL. This was deprecated in the Mitaka release.