Source code for ironic.common.tls_utils
# 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.
"""Shared TLS utilities for Ironic.
This module consolidates TLS-related constants, adapters, and helpers
that were previously duplicated across image_service, agent_client,
and wsgi_service.
"""
import ssl
from requests import adapters as req_adapters
from ironic.common.i18n import _
TLS_VERSION_MAP = {
'1.2': ssl.TLSVersion.TLSv1_2,
'1.3': ssl.TLSVersion.TLSv1_3,
}
[docs]
def check_tls_version_supported(version_str):
"""Validate that the requested TLS version is available.
Checks both compile-time flags and runtime crypto policy
to ensure the configured TLS version can actually be used.
Raises RuntimeError with a clear message at startup rather
than letting the service fail later with an opaque SSL
error.
"""
version = TLS_VERSION_MAP[version_str]
if not getattr(ssl, f'HAS_{version.name}', False):
raise RuntimeError(
_("TLS %(ver)s is not supported by the "
"installed version of OpenSSL "
"(ssl.HAS_%(attr)s is not set).")
% {'ver': version_str, 'attr': version.name}
)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
max_ver = ctx.maximum_version
if (max_ver != ssl.TLSVersion.MAXIMUM_SUPPORTED
and version > max_ver):
raise RuntimeError(
_("TLS %(ver)s exceeds the maximum TLS "
"version allowed by the system crypto "
"policy.")
% {'ver': version_str}
)
[docs]
class TLSHTTPAdapter(req_adapters.HTTPAdapter):
"""HTTPS adapter with configurable TLS settings."""
def __init__(self, ssl_context=None, **kwargs):
self._ssl_context = ssl_context
super().__init__(**kwargs)
[docs]
def init_poolmanager(self, *args, **kwargs):
if self._ssl_context:
kwargs['ssl_context'] = self._ssl_context
super().init_poolmanager(*args, **kwargs)
[docs]
def build_ssl_context(tls_minimum_version=None, tls_ciphers=None):
"""Create an ssl.SSLContext with the given TLS constraints.
:param tls_minimum_version: Minimum TLS version string
(e.g. '1.2', '1.3') or None.
:param tls_ciphers: OpenSSL cipher string or None.
:returns: A configured ssl.SSLContext, or None if both
params are falsy.
"""
if not tls_minimum_version and not tls_ciphers:
return None
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# Verification is handled separately via the requests
# verify parameter; default to permissive here so that
# the caller's verify setting remains the single source
# of truth.
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
if tls_minimum_version:
ctx.minimum_version = TLS_VERSION_MAP[tls_minimum_version]
if tls_ciphers:
ctx.set_ciphers(tls_ciphers)
return ctx