diff --git a/etc/octavia.conf b/etc/octavia.conf index 10afbb6e11..c82ca21ac3 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -68,6 +68,10 @@ # see https://cheatsheetseries.owasp.org/cheatsheets/TLS_Cipher_String_Cheat_Sheet.html # default_pool_ciphers = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256 +# Colon-separated list of disallowed ciphers. Ciphers specified here will not be +# allowed on listeners, pools, or the default values for either. +# tls_cipher_blacklist = + [database] # This line MUST be changed to actually run the plugin. # Example: diff --git a/octavia/api/v2/controllers/listener.py b/octavia/api/v2/controllers/listener.py index 7bac00f747..62ca98b68c 100644 --- a/octavia/api/v2/controllers/listener.py +++ b/octavia/api/v2/controllers/listener.py @@ -33,6 +33,7 @@ from octavia.common import data_models from octavia.common import exceptions from octavia.common import stats from octavia.common import utils as common_utils +from octavia.common import validate from octavia.db import api as db_api from octavia.db import prepare as db_prepare from octavia.i18n import _ @@ -223,6 +224,15 @@ class ListenersController(base.BaseController): "A client authentication CA reference is required to " "specify a client authentication revocation list.")) + # Check TLS cipher blacklist + if 'tls_ciphers' in listener_dict and listener_dict['tls_ciphers']: + rejected_ciphers = validate.check_cipher_blacklist( + listener_dict['tls_ciphers']) + if rejected_ciphers: + raise exceptions.ValidationException(detail=_( + 'The following ciphers have been blacklisted by an ' + 'administrator: ' + ', '.join(rejected_ciphers))) + # Validate the TLS containers sni_containers = listener_dict.pop('sni_containers', []) tls_refs = [sni['tls_container_id'] for sni in sni_containers] @@ -475,6 +485,15 @@ class ListenersController(base.BaseController): self._validate_cidr_compatible_with_vip( vip_address, listener.allowed_cidrs) + # Check TLS cipher blacklist + if listener.tls_ciphers: + rejected_ciphers = validate.check_cipher_blacklist( + listener.tls_ciphers) + if rejected_ciphers: + raise exceptions.ValidationException(detail=_( + 'The following ciphers have been blacklisted by an ' + 'administrator: ' + ', '.join(rejected_ciphers))) + def _set_default_on_none(self, listener): """Reset settings to their default values if None/null was passed in diff --git a/octavia/api/v2/controllers/pool.py b/octavia/api/v2/controllers/pool.py index 1ad210f7ef..b000c61f3e 100644 --- a/octavia/api/v2/controllers/pool.py +++ b/octavia/api/v2/controllers/pool.py @@ -122,6 +122,15 @@ class PoolsController(base.BaseController): pool_dict.get('ca_tls_certificate_id'), pool_dict.get('crl_container_id', None)) + # Check TLS cipher blacklist + if 'tls_ciphers' in pool_dict and pool_dict['tls_ciphers']: + rejected_ciphers = validate.check_cipher_blacklist( + pool_dict['tls_ciphers']) + if rejected_ciphers: + raise exceptions.ValidationException(detail=_( + 'The following ciphers have been blacklisted by an ' + 'administrator: ' + ', '.join(rejected_ciphers))) + try: return self.repositories.create_pool_on_load_balancer( lock_session, pool_dict, @@ -377,6 +386,15 @@ class PoolsController(base.BaseController): if ca_ref: self._validate_client_ca_and_crl_refs(ca_ref, crl_ref) + # Check TLS cipher blacklist + if pool.tls_ciphers: + rejected_ciphers = validate.check_cipher_blacklist( + pool.tls_ciphers) + if rejected_ciphers: + raise exceptions.ValidationException(detail=_( + "The following ciphers have been blacklisted by an " + "administrator: " + ', '.join(rejected_ciphers))) + @wsme_pecan.wsexpose(pool_types.PoolRootResponse, wtypes.text, body=pool_types.PoolRootPut, status_code=200) def put(self, id, pool_): diff --git a/octavia/common/config.py b/octavia/common/config.py index 5bb5f6435b..502f451ffd 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -31,6 +31,7 @@ import oslo_messaging as messaging from octavia.certificates.common import local from octavia.common import constants from octavia.common import utils +from octavia.common import validate from octavia.i18n import _ from octavia import version @@ -112,6 +113,9 @@ api_opts = [ default=constants.CIPHERS_OWASP_SUITE_B, help=_("Default OpenSSL cipher string (colon-separated) for " "new TLS-enabled pools.")), + cfg.StrOpt('tls_cipher_blacklist', default='', + help=_("Colon separated list of OpenSSL ciphers. " + "Usage of these ciphers will be blocked.")) ] # Options only used by the amphora agent @@ -822,6 +826,7 @@ def init(args, **kwargs): **kwargs) handle_deprecation_compatibility() setup_remote_debugger() + validate.check_default_ciphers_blacklist_conflict() def setup_logging(conf): diff --git a/octavia/common/validate.py b/octavia/common/validate.py index dc8e49690f..60e458aee8 100644 --- a/octavia/common/validate.py +++ b/octavia/common/validate.py @@ -433,3 +433,29 @@ def is_flavor_spares_compatible(flavor): if flavor.get(constants.COMPUTE_FLAVOR, None): return False return True + + +def check_cipher_blacklist(cipherstring): + ciphers = cipherstring.split(':') + blacklist = CONF.api_settings.tls_cipher_blacklist.split(':') + rejected = [] + for cipher in ciphers: + if cipher in blacklist: + rejected.append(cipher) + return rejected + + +def check_default_ciphers_blacklist_conflict(): + listener_rejected = check_cipher_blacklist( + CONF.api_settings.default_listener_ciphers) + if listener_rejected: + raise exceptions.ValidationException( + detail=_('Default listener ciphers conflict with blacklist. ' + 'Conflicting ciphers: ' + ', '.join(listener_rejected))) + + pool_rejected = check_cipher_blacklist( + CONF.api_settings.default_pool_ciphers) + if pool_rejected: + raise exceptions.ValidationException( + detail=_('Default pool ciphers conflict with blacklist. ' + 'Conflicting ciphers: ' + ', '.join(pool_rejected))) diff --git a/octavia/tests/unit/common/test_validations.py b/octavia/tests/unit/common/test_validations.py index 6e7d2cd7b1..d7d2e1d8f3 100644 --- a/octavia/tests/unit/common/test_validations.py +++ b/octavia/tests/unit/common/test_validations.py @@ -460,3 +460,13 @@ class TestValidations(base.TestCase): self.assertTrue(validate.is_flavor_spares_compatible(compat_flavor)) self.assertFalse( validate.is_flavor_spares_compatible(not_compat_flavor)) + + def test_check_default_ciphers_blacklist_conflict(self): + self.conf.config(group='api_settings', + tls_cipher_blacklist='PSK-AES128-CBC-SHA') + self.conf.config(group='api_settings', + default_listener_ciphers='ECDHE-ECDSA-AES256-SHA:' + 'PSK-AES128-CBC-SHA:TLS_AES_256_GCM_SHA384') + + self.assertRaises(exceptions.ValidationException, + validate.check_default_ciphers_blacklist_conflict) diff --git a/releasenotes/notes/tls-cipher-blacklist-b5a23ca38149f3b8.yaml b/releasenotes/notes/tls-cipher-blacklist-b5a23ca38149f3b8.yaml new file mode 100644 index 0000000000..4b82b0ca7a --- /dev/null +++ b/releasenotes/notes/tls-cipher-blacklist-b5a23ca38149f3b8.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added ``tls_cipher_blacklist`` to ``octavia.conf``. Listeners, pools, and + the default values for either will be blocked from using any of these ciphers. + By default, no ciphers are blacklisted.