|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 | import json |
| 16 | +import ssl |
16 | 17 | import threading |
17 | 18 |
|
18 | 19 | from cheroot.ssl import builtin |
|
25 | 26 | from ironic_python_agent.api import request_log |
26 | 27 | from ironic_python_agent import encoding |
27 | 28 | from ironic_python_agent.metrics_lib import metrics_utils |
| 29 | +from ironic_python_agent import utils |
28 | 30 |
|
29 | 31 |
|
30 | 32 | LOG = log.getLogger(__name__) |
| 33 | + |
| 34 | + |
| 35 | +class TLSEnforcingSSLAdapter(builtin.BuiltinSSLAdapter): |
| 36 | + """SSL adapter that enforces TLS version requirements. |
| 37 | +
|
| 38 | + This adapter extends cheroot's BuiltinSSLAdapter to support |
| 39 | + pre-configured SSL contexts with custom TLS version enforcement. |
| 40 | + """ |
| 41 | + |
| 42 | + def __init__(self, certificate, private_key, ssl_context=None, |
| 43 | + certificate_chain=None): |
| 44 | + """Initialize the adapter with optional SSL context. |
| 45 | +
|
| 46 | + :param certificate: Path to certificate file |
| 47 | + :param private_key: Path to private key file |
| 48 | + :param ssl_context: Optional pre-configured SSL context |
| 49 | + :param certificate_chain: Optional certificate chain file |
| 50 | + """ |
| 51 | + super().__init__(certificate, private_key, |
| 52 | + certificate_chain=certificate_chain) |
| 53 | + self._custom_context = ssl_context |
| 54 | + |
| 55 | + def wrap(self, sock): |
| 56 | + """Wrap socket with SSL using custom context if provided. |
| 57 | +
|
| 58 | + :param sock: The socket to wrap |
| 59 | + :returns: Tuple of (SSL-wrapped socket, SSL environ dict) |
| 60 | + """ |
| 61 | + if self._custom_context: |
| 62 | + s = self._custom_context.wrap_socket( |
| 63 | + sock, server_side=True, |
| 64 | + do_handshake_on_connect=True |
| 65 | + ) |
| 66 | + return s, self.get_environ(s) |
| 67 | + return super().wrap(sock) |
| 68 | + |
| 69 | + |
31 | 70 | _CUSTOM_MEDIA_TYPE = 'application/vnd.openstack.ironic-python-agent.v1+json' |
32 | 71 | _DOCS_URL = 'https://docs.openstack.org/ironic-python-agent' |
33 | 72 |
|
@@ -145,20 +184,38 @@ def start(self, tls_cert_file=None, tls_key_file=None): |
145 | 184 | server_name='ironic-python-agent') |
146 | 185 |
|
147 | 186 | if self.tls_cert_file and self.tls_key_file: |
148 | | - server.ssl_adapter = builtin.BuiltinSSLAdapter( |
| 187 | + # Create SSL context with TLS version enforcement |
| 188 | + ssl_context = utils.create_ssl_context('server') |
| 189 | + |
| 190 | + # Load certificate and key |
| 191 | + ssl_context.load_cert_chain( |
| 192 | + certfile=self.tls_cert_file, |
| 193 | + keyfile=self.tls_key_file |
| 194 | + ) |
| 195 | + |
| 196 | + # Disable client certificate verification |
| 197 | + # (agent is server, Ironic is client) |
| 198 | + ssl_context.check_hostname = False |
| 199 | + ssl_context.verify_mode = ssl.CERT_NONE |
| 200 | + |
| 201 | + server.ssl_adapter = TLSEnforcingSSLAdapter( |
149 | 202 | certificate=self.tls_cert_file, |
150 | | - private_key=self.tls_key_file |
| 203 | + private_key=self.tls_key_file, |
| 204 | + ssl_context=ssl_context |
151 | 205 | ) |
| 206 | + LOG.info('Started API service with TLS %s on port %s', |
| 207 | + self._conf.tls_min_version, |
| 208 | + self.agent.listen_address.port) |
| 209 | + else: |
| 210 | + LOG.info('Started API service without TLS on port %s', |
| 211 | + self.agent.listen_address.port) |
152 | 212 |
|
153 | 213 | self.server = server |
154 | 214 | self.server.prepare() |
155 | 215 | self.server_thread = threading.Thread(target=self.server.serve) |
156 | 216 | self.server_thread.daemon = True |
157 | 217 | self.server_thread.start() |
158 | 218 |
|
159 | | - LOG.info('Started API service on port %s', |
160 | | - self.agent.listen_address.port) |
161 | | - |
162 | 219 | def stop(self): |
163 | 220 | """Stop the API service.""" |
164 | 221 | LOG.debug("Stopping the API service.") |
|
0 commit comments