88import shutil
99import signal
1010import tempfile
11+ import threading
1112from typing import Dict , Tuple , Union
1213
1314import magic
8384from pycti .utils .opencti_stix2 import OpenCTIStix2
8485from pycti .utils .opencti_stix2_utils import OpenCTIStix2Utils
8586
87+ # Global singleton variables for proxy certificate management
88+ _PROXY_CERT_BUNDLE = None
89+ _PROXY_CERT_DIR = None
90+ _PROXY_CERT_LOCK = threading .Lock ()
91+ _PROXY_SIGNAL_HANDLERS_REGISTERED = False
92+
8693
8794def build_request_headers (token : str , custom_headers : str , app_logger ):
8895 headers_dict = {
@@ -171,18 +178,9 @@ def __init__(
171178 self .app_logger = self .logger_class ("api" )
172179 self .admin_logger = self .logger_class ("admin" )
173180
174- # Initialize temp certificate directory tracker
175- self .temp_cert_dir = None
176-
177181 # Setup proxy certificates if provided
178182 self ._setup_proxy_certificates ()
179183
180- # Register cleanup handlers for temp certificates
181- if self .temp_cert_dir :
182- atexit .register (self ._cleanup_temp_certificates )
183- signal .signal (signal .SIGTERM , self ._signal_handler )
184- signal .signal (signal .SIGINT , self ._signal_handler )
185-
186184 # Define API
187185 self .api_token = token
188186 self .api_url = url + "/graphql"
@@ -272,74 +270,101 @@ def _setup_proxy_certificates(self):
272270 Detects HTTPS_CA_CERTIFICATES environment variable and combines
273271 proxy certificates with system certificates for SSL verification.
274272 Supports both inline certificate content and file paths.
273+
274+ Uses a singleton pattern to ensure only one certificate bundle is created
275+ across all instances, avoiding resource leaks and conflicts.
275276 """
277+ global _PROXY_CERT_BUNDLE , _PROXY_CERT_DIR , _PROXY_SIGNAL_HANDLERS_REGISTERED
278+
276279 https_ca_certificates = os .getenv ("HTTPS_CA_CERTIFICATES" )
277280 if not https_ca_certificates :
278281 return
279282
280- try :
281- # Create secure temporary directory
282- cert_dir = tempfile .mkdtemp (prefix = "opencti_proxy_certs_" )
283- self .temp_cert_dir = cert_dir
284-
285- # Determine if HTTPS_CA_CERTIFICATES contains inline content or file path
286- cert_content = self ._get_certificate_content (https_ca_certificates )
287- if not cert_content :
288- self .app_logger .warning (
289- "Invalid HTTPS_CA_CERTIFICATES: not a valid certificate or file path" ,
290- {
291- "value" : (
292- https_ca_certificates [:50 ] + "..."
293- if len (https_ca_certificates ) > 50
294- else https_ca_certificates
295- )
296- },
283+ # Thread-safe check and setup
284+ with _PROXY_CERT_LOCK :
285+ # If already configured, reuse existing bundle
286+ if _PROXY_CERT_BUNDLE is not None :
287+ self .ssl_verify = _PROXY_CERT_BUNDLE
288+ self .app_logger .debug (
289+ "Reusing existing proxy certificate bundle" ,
290+ {"cert_bundle" : _PROXY_CERT_BUNDLE },
297291 )
298292 return
299293
300- # Write proxy certificate to temp file
301- proxy_cert_file = os .path .join (cert_dir , "proxy-ca.crt" )
302- with open (proxy_cert_file , "w" ) as f :
303- f .write (cert_content )
304-
305- # Find system certificates
306- system_cert_paths = [
307- "/etc/ssl/certs/ca-certificates.crt" , # Debian/Ubuntu
308- "/etc/pki/tls/certs/ca-bundle.crt" , # RHEL/CentOS
309- "/etc/ssl/cert.pem" , # Alpine/BSD
310- ]
311-
312- # Create combined certificate bundle
313- combined_cert_file = os .path .join (cert_dir , "combined-ca-bundle.crt" )
314- with open (combined_cert_file , "w" ) as combined :
315- # Add system certificates first
316- for system_path in system_cert_paths :
317- if os .path .exists (system_path ):
318- with open (system_path , "r" ) as sys_certs :
319- combined .write (sys_certs .read ())
320- combined .write ("\n " )
321- break
322-
323- # Add proxy certificate
324- combined .write (cert_content )
325-
326- # Update ssl_verify to use combined certificate bundle
327- self .ssl_verify = combined_cert_file
328-
329- # Set environment variables for urllib and other libraries
330- os .environ ["REQUESTS_CA_BUNDLE" ] = combined_cert_file
331- os .environ ["SSL_CERT_FILE" ] = combined_cert_file
332-
333- self .app_logger .info (
334- "Proxy certificates configured" ,
335- {"cert_bundle" : combined_cert_file },
336- )
294+ # First initialization - create the certificate bundle
295+ try :
296+ # Create secure temporary directory
297+ cert_dir = tempfile .mkdtemp (prefix = "opencti_proxy_certs_" )
298+
299+ # Determine if HTTPS_CA_CERTIFICATES contains inline content or file path
300+ cert_content = self ._get_certificate_content (https_ca_certificates )
301+ if not cert_content :
302+ self .app_logger .warning (
303+ "Invalid HTTPS_CA_CERTIFICATES: not a valid certificate or file path" ,
304+ {
305+ "value" : (
306+ https_ca_certificates [:50 ] + "..."
307+ if len (https_ca_certificates ) > 50
308+ else https_ca_certificates
309+ )
310+ },
311+ )
312+ return
313+
314+ # Write proxy certificate to temp file
315+ proxy_cert_file = os .path .join (cert_dir , "proxy-ca.crt" )
316+ with open (proxy_cert_file , "w" ) as f :
317+ f .write (cert_content )
318+
319+ # Find system certificates
320+ system_cert_paths = [
321+ "/etc/ssl/certs/ca-certificates.crt" , # Debian/Ubuntu
322+ "/etc/pki/tls/certs/ca-bundle.crt" , # RHEL/CentOS
323+ "/etc/ssl/cert.pem" , # Alpine/BSD
324+ ]
325+
326+ # Create combined certificate bundle
327+ combined_cert_file = os .path .join (cert_dir , "combined-ca-bundle.crt" )
328+ with open (combined_cert_file , "w" ) as combined :
329+ # Add system certificates first
330+ for system_path in system_cert_paths :
331+ if os .path .exists (system_path ):
332+ with open (system_path , "r" ) as sys_certs :
333+ combined .write (sys_certs .read ())
334+ combined .write ("\n " )
335+ break
336+
337+ # Add proxy certificate
338+ combined .write (cert_content )
339+
340+ # Update global singleton variables
341+ _PROXY_CERT_BUNDLE = combined_cert_file
342+ _PROXY_CERT_DIR = cert_dir
343+ self .ssl_verify = combined_cert_file
344+
345+ # Set environment variables for urllib and other libraries
346+ os .environ ["REQUESTS_CA_BUNDLE" ] = combined_cert_file
347+ os .environ ["SSL_CERT_FILE" ] = combined_cert_file
348+
349+ # Register cleanup handlers only once
350+ atexit .register (_cleanup_proxy_certificates )
351+
352+ # Register signal handlers only once
353+ if not _PROXY_SIGNAL_HANDLERS_REGISTERED :
354+ signal .signal (signal .SIGTERM , _signal_handler_proxy_cleanup )
355+ signal .signal (signal .SIGINT , _signal_handler_proxy_cleanup )
356+ _PROXY_SIGNAL_HANDLERS_REGISTERED = True
357+
358+ self .app_logger .info (
359+ "Proxy certificates configured" ,
360+ {"cert_bundle" : combined_cert_file },
361+ )
337362
338- except Exception as e :
339- self .app_logger .error (
340- "Failed to setup proxy certificates" , {"error" : str (e )}
341- )
342- raise
363+ except Exception as e :
364+ self .app_logger .error (
365+ "Failed to setup proxy certificates" , {"error" : str (e )}
366+ )
367+ raise
343368
344369 def _get_certificate_content (self , https_ca_certificates ):
345370 """Extract certificate content from environment variable.
@@ -353,7 +378,7 @@ def _get_certificate_content(self, https_ca_certificates):
353378 """
354379 # Strip whitespace once at the beginning
355380 stripped_https_ca_certificates = https_ca_certificates .strip ()
356-
381+
357382 # Check if it's inline certificate content (starts with PEM header)
358383 if stripped_https_ca_certificates .startswith ("-----BEGIN CERTIFICATE-----" ):
359384 self .app_logger .debug (
@@ -390,43 +415,6 @@ def _get_certificate_content(self, https_ca_certificates):
390415 # Neither inline content nor valid file path
391416 return None
392417
393- def _cleanup_temp_certificates (self ):
394- """Clean up temporary certificate directory.
395-
396- This method is called on normal program exit via atexit
397- or when receiving termination signals (SIGTERM/SIGINT).
398- """
399- if self .temp_cert_dir and os .path .exists (self .temp_cert_dir ):
400- try :
401- shutil .rmtree (self .temp_cert_dir )
402- self .app_logger .debug (
403- "Cleaned up temporary certificates" ,
404- {"cert_dir" : self .temp_cert_dir }
405- )
406- except Exception as e :
407- self .app_logger .warning (
408- "Failed to cleanup temporary certificates" ,
409- {"cert_dir" : self .temp_cert_dir , "error" : str (e )}
410- )
411- finally :
412- self .temp_cert_dir = None
413-
414- def _signal_handler (self , signum , frame ):
415- """Handle termination signals (SIGTERM/SIGINT).
416-
417- Performs cleanup and then raises SystemExit to allow
418- normal shutdown procedures to complete.
419-
420- :param signum: Signal number
421- :param frame: Current stack frame
422- """
423- self .app_logger .info (
424- "Received termination signal, cleaning up" ,
425- {"signal" : signum }
426- )
427- self ._cleanup_temp_certificates ()
428- raise SystemExit (0 )
429-
430418 def set_applicant_id_header (self , applicant_id ):
431419 self .request_headers ["opencti-applicant-id" ] = applicant_id
432420
@@ -1062,3 +1050,33 @@ def get_attribute_in_mitre_extension(key, object) -> any:
10621050 "extension-definition--322b8f77-262a-4cb8-a915-1e441e00329b"
10631051 ][key ]
10641052 return None
1053+
1054+
1055+ # Global cleanup functions for proxy certificates singleton
1056+ def _cleanup_proxy_certificates ():
1057+ """Clean up temporary certificate directory for proxy certificates.
1058+
1059+ This function is called on normal program exit via atexit.
1060+ """
1061+ global _PROXY_CERT_DIR
1062+ if _PROXY_CERT_DIR and os .path .exists (_PROXY_CERT_DIR ):
1063+ try :
1064+ shutil .rmtree (_PROXY_CERT_DIR )
1065+ except Exception :
1066+ # Silently fail cleanup - best effort
1067+ pass
1068+ finally :
1069+ _PROXY_CERT_DIR = None
1070+
1071+
1072+ def _signal_handler_proxy_cleanup (signum , frame ):
1073+ """Handle termination signals (SIGTERM/SIGINT) for proxy certificate cleanup.
1074+
1075+ Performs cleanup and then raises SystemExit to allow
1076+ normal shutdown procedures to complete.
1077+
1078+ :param signum: Signal number
1079+ :param frame: Current stack frame
1080+ """
1081+ _cleanup_proxy_certificates ()
1082+ raise SystemExit (0 )
0 commit comments