From a406b39d93e621a4a9d6320c4ff7e7a125dba6e8 Mon Sep 17 00:00:00 2001 From: Brant Knudson Date: Wed, 15 Oct 2014 14:53:32 -0500 Subject: [PATCH 1/1] Configurable SSL protocol and ciphers The deployer couldn't configure the SSL protocol and ciphers when running keystone-all. With this change, two new config options are available in the [ssl] section, protocol_version and ciphers which allow setting the SSL version and allowed ciphers for the SSL socket. DocImpact SecurityImpact Change-Id: I5fa1c6c6da87023d7ec868fa6be5d4fcc9331224 Closes-Bug: #1381365 --- bin/keystone-all | 4 ++- keystone/common/config.py | 10 ++++++++ keystone/common/environment/eventlet_server.py | 31 ++++++++++++++++++++++-- keystone/tests/ksfixtures/appserver.py | 9 +++++-- keystone/tests/test_ssl.py | 31 ++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/bin/keystone-all b/bin/keystone-all index 89e5128..07ddb81 100755 --- a/bin/keystone-all +++ b/bin/keystone-all @@ -80,7 +80,9 @@ def create_server(conf, name, host, port, workers): keepidle=CONF.tcp_keepidle) if CONF.ssl.enable: server.set_ssl(CONF.ssl.certfile, CONF.ssl.keyfile, - CONF.ssl.ca_certs, CONF.ssl.cert_required) + CONF.ssl.ca_certs, CONF.ssl.cert_required, + protocol_version=CONF.ssl.protocol_version, + ciphers=CONF.ssl.ciphers) return name, ServerWrapper(server, workers) diff --git a/keystone/common/config.py b/keystone/common/config.py index b43ee38..e96fcdd 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -382,6 +382,16 @@ FILE_OPTIONS = { help='Path of the CA key file for SSL.'), cfg.BoolOpt('cert_required', default=False, help='Require client certificate.'), + cfg.StrOpt('protocol_version', + help="SSL version. The allowed values are the protocols " + "provided by the Python standard library's `ssl` " + "library. For example, 'TLSv1' for " + "ssl.PROTOCOL_TLSv1. The default is to use the " + "standard library's default for SSL version " + "(SSLv23)."), + cfg.StrOpt('ciphers', + help="The available ciphers for the connection. The format " + "is the OpenSSL cipher list format."), cfg.IntOpt('key_size', default=1024, help='SSL key length (in bits) (auto generated ' 'certificate).'), diff --git a/keystone/common/environment/eventlet_server.py b/keystone/common/environment/eventlet_server.py index fa6d58f..218d2eb 100644 --- a/keystone/common/environment/eventlet_server.py +++ b/keystone/common/environment/eventlet_server.py @@ -122,11 +122,18 @@ class Server(object): else: cert_reqs = ssl.CERT_NONE + wrap_kwargs = {} + if self.protocol_version is not None: + wrap_kwargs['ssl_version'] = self.protocol_version + if self.ciphers: + wrap_kwargs['ciphers'] = self.ciphers + dup_socket = eventlet.wrap_ssl(dup_socket, certfile=self.certfile, keyfile=self.keyfile, server_side=True, cert_reqs=cert_reqs, - ca_certs=self.ca_certs) + ca_certs=self.ca_certs, + **wrap_kwargs) # Optionally enable keepalive on the wsgi socket. if self.keepalive: @@ -142,11 +149,31 @@ class Server(object): dup_socket) def set_ssl(self, certfile, keyfile=None, ca_certs=None, - cert_required=True): + cert_required=True, protocol_version=None, ciphers=None): + PROTOCOL_PREFIX = 'PROTOCOL_' + self.certfile = certfile self.keyfile = keyfile self.ca_certs = ca_certs self.cert_required = cert_required + if protocol_version: + try: + protocol_version = getattr( + ssl, '%s%s' % (PROTOCOL_PREFIX, protocol_version)) + except AttributeError: + allowed_protocols = ', '.join( + [p[len(PROTOCOL_PREFIX):] for p in dir(ssl) + if p.startswith(PROTOCOL_PREFIX)]) + raise Exception( + _('Invalid protocol_version %(version)s. Allowed ' + 'protocols: %(allowed)s') % + {'version': protocol_version, + 'allowed': allowed_protocols}) + self.protocol_version = protocol_version + self.ciphers = ciphers + + LOG.debug('SSL protocol=%s ciphers=%s', self.protocol_version, + self.ciphers) self.do_ssl = True def stop(self): diff --git a/keystone/tests/ksfixtures/appserver.py b/keystone/tests/ksfixtures/appserver.py index 3d2307e..8c6e094 100644 --- a/keystone/tests/ksfixtures/appserver.py +++ b/keystone/tests/ksfixtures/appserver.py @@ -33,7 +33,8 @@ class AppServer(fixtures.Fixture): """ def __init__(self, config, name, cert=None, key=None, ca=None, - cert_required=False, host='127.0.0.1', port=0): + cert_required=False, host='127.0.0.1', port=0, + protocol_version=None, ciphers=None): super(AppServer, self).__init__() self.config = config self.name = name @@ -43,6 +44,8 @@ class AppServer(fixtures.Fixture): self.cert_required = cert_required self.host = host self.port = port + self.protocol_version = protocol_version + self.ciphers = ciphers def setUp(self): super(AppServer, self).setUp() @@ -67,7 +70,9 @@ class AppServer(fixtures.Fixture): self.server.set_ssl(certfile=self.cert, keyfile=self.key, ca_certs=self.ca, - cert_required=self.cert_required) + cert_required=self.cert_required, + protocol_version=self.protocol_version, + ciphers=self.ciphers) def _update_config_opt(self): """Updates the config with the actual port used.""" diff --git a/keystone/tests/test_ssl.py b/keystone/tests/test_ssl.py index ecfdf3b..c385bbf 100644 --- a/keystone/tests/test_ssl.py +++ b/keystone/tests/test_ssl.py @@ -15,6 +15,9 @@ import os import ssl +import uuid + +from testtools import matchers from keystone.common import environment from keystone import config @@ -154,3 +157,31 @@ class SSLTestCase(tests.TestCase): self.fail('Public API shoulda failed with SSL handshake!') except ssl.SSLError: pass + + def test_protocol_version_valid(self): + PROTOCOL_PREFIX = 'PROTOCOL_' + valid_protocol_versions = [p[len(PROTOCOL_PREFIX):] for p in dir(ssl) + if p.startswith(PROTOCOL_PREFIX)] + + paste_conf = self._paste_config('keystone') + + for protocol_version in valid_protocol_versions: + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA, + protocol_version=protocol_version) + + with appserver.AppServer(paste_conf, appserver.ADMIN, + **ssl_kwargs): + pass + + def test_conf_invalid_protocol(self): + paste_conf = self._paste_config('keystone') + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA, + protocol_version=uuid.uuid4().hex) + + try: + with appserver.AppServer(paste_conf, appserver.ADMIN, + **ssl_kwargs): + self.fail('Expected AppServer to raise.') + except Exception as e: + matcher = matchers.StartsWith('Invalid protocol_version') + self.assertThat(e.args[0], matcher) -- 1.7.9.5