This is an automated email from the ASF dual-hosted git repository.

eze pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new ffd8d8ba34 Parallell ssl cert load (#12998)
ffd8d8ba34 is described below

commit ffd8d8ba3409bdff2de4a80c8c476f05409af520
Author: Evan Zelkowitz <[email protected]>
AuthorDate: Mon Mar 23 10:07:33 2026 -0600

    Parallell ssl cert load (#12998)
    
    * Add parallel ssl cert loading
---
 doc/admin-guide/files/records.yaml.en.rst         |  7 ++
 include/iocore/net/SSLMultiCertConfigLoader.h     | 10 ++-
 src/iocore/net/P_SSLConfig.h                      |  1 +
 src/iocore/net/QUICMultiCertConfigLoader.cc       |  2 +-
 src/iocore/net/SSLConfig.cc                       |  9 ++-
 src/iocore/net/SSLUtils.cc                        | 93 ++++++++++++++++++-----
 src/records/RecordsConfig.cc                      |  4 +-
 tests/gold_tests/tls/ssl_multicert_loader.test.py | 44 ++++++++++-
 8 files changed, 148 insertions(+), 22 deletions(-)

diff --git a/doc/admin-guide/files/records.yaml.en.rst 
b/doc/admin-guide/files/records.yaml.en.rst
index 492dc9849f..f64028b647 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -4089,6 +4089,13 @@ SSL Termination
    :file:`ssl_multicert.yaml` file successfully load.  If false (``0``), SSL 
certificate
    load failures will not prevent |TS| from starting.
 
+.. ts:cv:: CONFIG proxy.config.ssl.server.multicert.concurrency INT 1
+
+   Controls how many threads are used to load SSL certificates from 
:file:`ssl_multicert.yaml`
+   during configuration reloads.  On first startup, |TS| always uses all 
available CPU cores
+   regardless of this setting.  Set to ``0`` to automatically use the number 
of hardware
+   threads.  Default ``1`` (single-threaded reloads).
+
 .. ts:cv:: CONFIG proxy.config.ssl.server.cert.path STRING /config
 
    The location of the SSL certificates and chains used for accepting
diff --git a/include/iocore/net/SSLMultiCertConfigLoader.h 
b/include/iocore/net/SSLMultiCertConfigLoader.h
index b2b7fc246e..d0f68469ce 100644
--- a/include/iocore/net/SSLMultiCertConfigLoader.h
+++ b/include/iocore/net/SSLMultiCertConfigLoader.h
@@ -25,10 +25,12 @@
 
 #include "iocore/net/SSLTypes.h"
 #include "tsutil/DbgCtl.h"
+#include "config/ssl_multicert.h"
 
 #include <openssl/ssl.h>
 #include <swoc/Errata.h>
 
+#include <mutex>
 #include <string>
 #include <set>
 #include <vector>
@@ -51,7 +53,7 @@ public:
   SSLMultiCertConfigLoader(const SSLConfigParams *p) : _params(p) {}
   virtual ~SSLMultiCertConfigLoader(){};
 
-  swoc::Errata load(SSLCertLookup *lookup);
+  swoc::Errata load(SSLCertLookup *lookup, bool firstLoad = false);
 
   virtual SSL_CTX *default_server_ssl_ctx();
 
@@ -88,6 +90,12 @@ private:
   virtual bool          _store_ssl_ctx(SSLCertLookup *lookup, const 
shared_SSLMultiCertConfigParams &ssl_multi_cert_params);
   bool _prep_ssl_ctx(const shared_SSLMultiCertConfigParams 
&sslMultCertSettings, SSLMultiCertConfigLoader::CertLoadData &data,
                      std::set<std::string> &common_names, 
std::unordered_map<int, std::set<std::string>> &unique_names);
+
+  void _load_items(SSLCertLookup *lookup, 
config::SSLMultiCertConfig::const_iterator begin,
+                   config::SSLMultiCertConfig::const_iterator end, int 
base_index, swoc::Errata &errata);
+
+  std::mutex _loader_mutex;
+
   virtual void _set_handshake_callbacks(SSL_CTX *ctx);
   virtual bool _setup_session_cache(SSL_CTX *ctx);
   virtual bool _setup_dialog(SSL_CTX *ctx, const SSLMultiCertConfigParams 
*sslMultCertSettings);
diff --git a/src/iocore/net/P_SSLConfig.h b/src/iocore/net/P_SSLConfig.h
index 135d25a5c9..0d6ee6b14e 100644
--- a/src/iocore/net/P_SSLConfig.h
+++ b/src/iocore/net/P_SSLConfig.h
@@ -66,6 +66,7 @@ struct SSLConfigParams : public ConfigInfo {
   char *cipherSuite;
   char *client_cipherSuite;
   int   configExitOnLoadError;
+  int   configLoadConcurrency;
   int   clientCertLevel;
   int   verify_depth;
   int   ssl_origin_session_cache{0};
diff --git a/src/iocore/net/QUICMultiCertConfigLoader.cc 
b/src/iocore/net/QUICMultiCertConfigLoader.cc
index 34a39115c4..8e5bbe1dca 100644
--- a/src/iocore/net/QUICMultiCertConfigLoader.cc
+++ b/src/iocore/net/QUICMultiCertConfigLoader.cc
@@ -45,7 +45,7 @@ QUICCertConfig::reconfigure(ConfigContext ctx)
   SSLCertLookup           *lookup = new SSLCertLookup();
 
   QUICMultiCertConfigLoader loader(params);
-  auto                      errata = loader.load(lookup);
+  auto                      errata = loader.load(lookup, _config_id == 0);
   if (!lookup->is_valid || (errata.has_severity() && errata.severity() >= 
ERRATA_ERROR)) {
     retStatus = false;
   }
diff --git a/src/iocore/net/SSLConfig.cc b/src/iocore/net/SSLConfig.cc
index 6aaf8c2374..936821b41b 100644
--- a/src/iocore/net/SSLConfig.cc
+++ b/src/iocore/net/SSLConfig.cc
@@ -46,9 +46,11 @@
 #include "mgmt/config/ConfigRegistry.h"
 
 #include <openssl/pem.h>
+#include <algorithm>
 #include <array>
 #include <cstring>
 #include <cmath>
+#include <thread>
 #include <unordered_map>
 
 int                SSLConfig::config_index                           = 0;
@@ -125,6 +127,7 @@ SSLConfigParams::reset()
   ssl_ctx_options                                      = SSL_OP_NO_SSLv2 | 
SSL_OP_NO_SSLv3;
   ssl_client_ctx_options                               = SSL_OP_NO_SSLv2 | 
SSL_OP_NO_SSLv3;
   configExitOnLoadError                                = 1;
+  configLoadConcurrency                                = 1;
 }
 
 void
@@ -431,6 +434,10 @@ SSLConfigParams::initialize()
 
   configFilePath        = 
ats_stringdup(RecConfigReadConfigPath("proxy.config.ssl.server.multicert.filename"));
   configExitOnLoadError = 
RecGetRecordInt("proxy.config.ssl.server.multicert.exit_on_load_fail").value_or(0);
+  configLoadConcurrency = 
RecGetRecordInt("proxy.config.ssl.server.multicert.concurrency").value_or(1);
+  if (configLoadConcurrency == 0) {
+    configLoadConcurrency = 
std::clamp(static_cast<int>(std::thread::hardware_concurrency()), 1, 256);
+  }
 
   {
     auto 
rec_str{RecGetRecordStringAlloc("proxy.config.ssl.server.private_key.path")};
@@ -671,7 +678,7 @@ SSLCertificateConfig::reconfigure(ConfigContext ctx)
     ink_hrtime_sleep(HRTIME_SECONDS(secs));
   }
 
-  auto errata = SSLMultiCertConfigLoader(params).load(lookup);
+  auto errata = SSLMultiCertConfigLoader(params).load(lookup, configid == 0);
   if (!lookup->is_valid || (errata.has_severity() && errata.severity() >= 
ERRATA_ERROR)) {
     retStatus = false;
   }
diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc
index 9bca4c3086..ee2231615a 100644
--- a/src/iocore/net/SSLUtils.cc
+++ b/src/iocore/net/SSLUtils.cc
@@ -69,6 +69,8 @@
 #include <openssl/ts.h>
 #endif
 
+#include <algorithm>
+#include <thread>
 #include <utility>
 #include <string>
 #include <unistd.h>
@@ -1599,11 +1601,20 @@ SSLMultiCertConfigLoader::_store_ssl_ctx(SSLCertLookup 
*lookup, const shared_SSL
   SSLMultiCertConfigLoader::CertLoadData         data;
 
   if (!this->_prep_ssl_ctx(sslMultCertSettings, data, common_names, 
unique_names)) {
-    lookup->is_valid = false;
+    {
+      std::lock_guard<std::mutex> lock(_loader_mutex);
+      lookup->is_valid = false;
+    }
     return false;
   }
 
   std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(data, 
sslMultCertSettings.get());
+
+  // Serialize all mutations to the shared SSLCertLookup.
+  // The expensive work above (_prep_ssl_ctx + init_server_ssl_ctx) runs
+  // without the lock, allowing parallel cert loading across threads.
+  std::lock_guard<std::mutex> lock(_loader_mutex);
+
   for (const auto &loadingctx : ctxs) {
     if (!sslMultCertSettings ||
         !this->_store_single_ssl_ctx(lookup, sslMultCertSettings, 
shared_SSL_CTX{loadingctx.ctx, SSL_CTX_free}, loadingctx.ctx_type,
@@ -1782,7 +1793,7 @@ 
SSLMultiCertConfigLoader::_store_single_ssl_ctx(SSLCertLookup *lookup, const sha
 }
 
 swoc::Errata
-SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
+SSLMultiCertConfigLoader::load(SSLCertLookup *lookup, bool firstLoad)
 {
   const SSLConfigParams *params = this->_params;
 
@@ -1806,10 +1817,69 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
   }
 
   swoc::Errata errata(ERRATA_NOTE);
-  int          item_num = 0;
 
-  for (const auto &item : parse_result.value) {
+  static constexpr int MAX_LOAD_THREADS = 256;
+
+  int num_threads = params->configLoadConcurrency;
+  if (firstLoad) {
+    num_threads = 
std::clamp(static_cast<int>(std::thread::hardware_concurrency()), 1, 
MAX_LOAD_THREADS);
+  }
+  num_threads = std::min(num_threads, 
static_cast<int>(parse_result.value.size()));
+
+  if (num_threads > 1 && parse_result.value.size() > 1) {
+    std::size_t bucket_size = parse_result.value.size() / num_threads;
+    std::size_t remainder   = parse_result.value.size() % num_threads;
+    auto        current     = parse_result.value.cbegin();
+
+    std::vector<std::thread> threads;
+    Note("(%s) loading %zu certs with %d threads", this->_debug_tag(), 
parse_result.value.size(), num_threads);
+
+    for (int t = 0; t < num_threads; ++t) {
+      std::size_t this_bucket = bucket_size + (static_cast<std::size_t>(t) < 
remainder ? 1 : 0);
+      auto        end         = current + this_bucket;
+      int         base_index  = 
static_cast<int>(std::distance(parse_result.value.cbegin(), current));
+      threads.emplace_back(&SSLMultiCertConfigLoader::_load_items, this, 
lookup, current, end, base_index, std::ref(errata));
+      current = end;
+    }
+
+    for (auto &th : threads) {
+      th.join();
+    }
+
+    Note("(%s) loaded %zu certs in %d threads", this->_debug_tag(), 
parse_result.value.size(), num_threads);
+  } else {
+    _load_items(lookup, parse_result.value.cbegin(), 
parse_result.value.cend(), 0, errata);
+    Note("(%s) loaded %zu certs (single-threaded)", this->_debug_tag(), 
parse_result.value.size());
+  }
+
+  // We *must* have a default context even if it can't possibly work. The 
default context is used to
+  // bootstrap the SSL handshake so that we can subsequently do the SNI lookup 
to switch to the real
+  // context.
+  if (lookup->ssl_default == nullptr) {
+    shared_SSLMultiCertConfigParams sslMultiCertSettings(new 
SSLMultiCertConfigParams);
+    sslMultiCertSettings->addr = ats_strdup("*");
+    if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
+      errata.note(ERRATA_ERROR, "failed set default context");
+    }
+  }
+
+  return errata;
+}
+
+void
+SSLMultiCertConfigLoader::_load_items(SSLCertLookup *lookup, 
config::SSLMultiCertConfig::const_iterator begin,
+                                      
config::SSLMultiCertConfig::const_iterator end, int base_index, swoc::Errata 
&errata)
+{
+  // Each thread needs its own elevated privileges since POSIX capabilities 
are per-thread
+  uint32_t elevate_setting = 0;
+  elevate_setting          = 
RecGetRecordInt("proxy.config.ssl.cert.load_elevated").value_or(0);
+  ElevateAccess elevate_access(elevate_setting ? ElevateAccess::FILE_PRIVILEGE 
: 0);
+
+  int item_num = base_index;
+  for (auto it = begin; it != end; ++it) {
     item_num++;
+    const auto &item = *it;
+
     shared_SSLMultiCertConfigParams sslMultiCertSettings = 
std::make_shared<SSLMultiCertConfigParams>();
 
     if (!item.ssl_cert_name.empty()) {
@@ -1846,25 +1916,14 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
     // There must be a certificate specified unless the tunnel action is set.
     if (sslMultiCertSettings->cert || sslMultiCertSettings->opt == 
SSLCertContextOption::OPT_TUNNEL) {
       if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
+        std::lock_guard<std::mutex> lock(_loader_mutex);
         errata.note(ERRATA_ERROR, "Failed to load certificate at item {}", 
item_num);
       }
     } else {
+      std::lock_guard<std::mutex> lock(_loader_mutex);
       errata.note(ERRATA_WARN, "No ssl_cert_name specified and no tunnel 
action set at item {}", item_num);
     }
   }
-
-  // We *must* have a default context even if it can't possibly work. The 
default context is used to
-  // bootstrap the SSL handshake so that we can subsequently do the SNI lookup 
to switch to the real
-  // context.
-  if (lookup->ssl_default == nullptr) {
-    shared_SSLMultiCertConfigParams sslMultiCertSettings(new 
SSLMultiCertConfigParams);
-    sslMultiCertSettings->addr = ats_strdup("*");
-    if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
-      errata.note(ERRATA_ERROR, "failed set default context");
-    }
-  }
-
-  return errata;
 }
 
 // Release SSL_CTX and the associated data. This works for both
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index f2c85649a7..68af49ba18 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -1182,7 +1182,9 @@ static constexpr RecordElement RecordsConfig[] =
   {RECT_CONFIG, "proxy.config.ssl.server.multicert.filename", RECD_STRING, 
ts::filename::SSL_MULTICERT, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, 
RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.ssl.server.multicert.exit_on_load_fail", 
RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
-,
+  ,
+  {RECT_CONFIG, "proxy.config.ssl.server.multicert.concurrency", RECD_INT, 
"1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-256]", RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.ssl.servername.filename", RECD_STRING, 
ts::filename::SNI, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.ssl.server.ticket_key.filename", RECD_STRING, 
nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
diff --git a/tests/gold_tests/tls/ssl_multicert_loader.test.py 
b/tests/gold_tests/tls/ssl_multicert_loader.test.py
index 6b74da7c53..5eb95a7f53 100644
--- a/tests/gold_tests/tls/ssl_multicert_loader.test.py
+++ b/tests/gold_tests/tls/ssl_multicert_loader.test.py
@@ -22,7 +22,7 @@ sni_domain = 'example.com'
 
 ts = Test.MakeATSProcess("ts", enable_tls=True)
 server = Test.MakeOriginServer("server")
-server2 = Test.MakeOriginServer("server3")
+server2 = Test.MakeOriginServer("server2")
 request_header = {"headers": f"GET / HTTP/1.1\r\nHost: {sni_domain}\r\n\r\n", 
"timestamp": "1469733493.993", "body": ""}
 
 response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", 
"timestamp": "1469733493.993", "body": ""}
@@ -123,3 +123,45 @@ ts2.Ready = 0  # Need this to be 0 because we are testing 
shutdown, this is to m
 ts2.Disk.traffic_out.Content = Testers.ExcludesExpression(
     'Traffic Server is fully initialized', 'process should fail when invalid 
certificate specified')
 ts2.Disk.diags_log.Content = Testers.IncludesExpression('EMERGENCY: failed to 
load SSL certificate file', 'check diags.log"')
+
+##########################################################################
+# Verify parallel cert loading on startup (firstLoad uses hardware_concurrency,
+# not the configured concurrency value, so the thread count is host-dependent)
+
+ts3 = Test.MakeATSProcess("ts3", enable_tls=True)
+server3 = Test.MakeOriginServer("server3")
+server3.addResponse("sessionlog.json", request_header, response_header)
+
+ts3.Disk.records_config.update(
+    {
+        'proxy.config.ssl.server.cert.path': f'{ts3.Variables.SSLDir}',
+        'proxy.config.ssl.server.private_key.path': f'{ts3.Variables.SSLDir}',
+    })
+
+ts3.addDefaultSSLFiles()
+
+ts3.Disk.remap_config.AddLine(f'map / 
http://127.0.0.1:{server3.Variables.Port}')
+
+# Need at least 2 certs for multi-threading to kick in
+ts3.Disk.ssl_multicert_yaml.AddLines(
+    """
+ssl_multicert:
+  - dest_ip: "*"
+    ssl_cert_name: server.pem
+    ssl_key_name: server.key
+  - ssl_cert_name: server.pem
+    ssl_key_name: server.key
+""".split("\n"))
+
+tr5 = Test.AddTestRun("Verify parallel cert loading")
+tr5.Processes.Default.StartBefore(ts3)
+tr5.Processes.Default.StartBefore(server3)
+tr5.StillRunningAfter = ts3
+tr5.StillRunningAfter = server3
+tr5.MakeCurlCommand(
+    f"-q -s -v -k --resolve '{sni_domain}:{ts3.Variables.ssl_port}:127.0.0.1' 
https://{sni_domain}:{ts3.Variables.ssl_port}";,
+    ts=ts3)
+tr5.Processes.Default.ReturnCode = 0
+tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not 
Connect", "Check response")
+tr5.Processes.Default.Streams.stderr = 
Testers.IncludesExpression(f"CN={sni_domain}", "Check response")
+ts3.Disk.diags_log.Content = Testers.IncludesExpression('loaded 2 certs', 
'verify certs were loaded successfully')

Reply via email to