Skip to content

Commit ffd8d8b

Browse files
authored
Parallell ssl cert load (#12998)
* Add parallel ssl cert loading
1 parent 8d75849 commit ffd8d8b

8 files changed

Lines changed: 148 additions & 22 deletions

File tree

doc/admin-guide/files/records.yaml.en.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4089,6 +4089,13 @@ SSL Termination
40894089
:file:`ssl_multicert.yaml` file successfully load. If false (``0``), SSL certificate
40904090
load failures will not prevent |TS| from starting.
40914091

4092+
.. ts:cv:: CONFIG proxy.config.ssl.server.multicert.concurrency INT 1
4093+
4094+
Controls how many threads are used to load SSL certificates from :file:`ssl_multicert.yaml`
4095+
during configuration reloads. On first startup, |TS| always uses all available CPU cores
4096+
regardless of this setting. Set to ``0`` to automatically use the number of hardware
4097+
threads. Default ``1`` (single-threaded reloads).
4098+
40924099
.. ts:cv:: CONFIG proxy.config.ssl.server.cert.path STRING /config
40934100
40944101
The location of the SSL certificates and chains used for accepting

include/iocore/net/SSLMultiCertConfigLoader.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525

2626
#include "iocore/net/SSLTypes.h"
2727
#include "tsutil/DbgCtl.h"
28+
#include "config/ssl_multicert.h"
2829

2930
#include <openssl/ssl.h>
3031
#include <swoc/Errata.h>
3132

33+
#include <mutex>
3234
#include <string>
3335
#include <set>
3436
#include <vector>
@@ -51,7 +53,7 @@ class SSLMultiCertConfigLoader
5153
SSLMultiCertConfigLoader(const SSLConfigParams *p) : _params(p) {}
5254
virtual ~SSLMultiCertConfigLoader(){};
5355

54-
swoc::Errata load(SSLCertLookup *lookup);
56+
swoc::Errata load(SSLCertLookup *lookup, bool firstLoad = false);
5557

5658
virtual SSL_CTX *default_server_ssl_ctx();
5759

@@ -88,6 +90,12 @@ class SSLMultiCertConfigLoader
8890
virtual bool _store_ssl_ctx(SSLCertLookup *lookup, const shared_SSLMultiCertConfigParams &ssl_multi_cert_params);
8991
bool _prep_ssl_ctx(const shared_SSLMultiCertConfigParams &sslMultCertSettings, SSLMultiCertConfigLoader::CertLoadData &data,
9092
std::set<std::string> &common_names, std::unordered_map<int, std::set<std::string>> &unique_names);
93+
94+
void _load_items(SSLCertLookup *lookup, config::SSLMultiCertConfig::const_iterator begin,
95+
config::SSLMultiCertConfig::const_iterator end, int base_index, swoc::Errata &errata);
96+
97+
std::mutex _loader_mutex;
98+
9199
virtual void _set_handshake_callbacks(SSL_CTX *ctx);
92100
virtual bool _setup_session_cache(SSL_CTX *ctx);
93101
virtual bool _setup_dialog(SSL_CTX *ctx, const SSLMultiCertConfigParams *sslMultCertSettings);

src/iocore/net/P_SSLConfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ struct SSLConfigParams : public ConfigInfo {
6666
char *cipherSuite;
6767
char *client_cipherSuite;
6868
int configExitOnLoadError;
69+
int configLoadConcurrency;
6970
int clientCertLevel;
7071
int verify_depth;
7172
int ssl_origin_session_cache{0};

src/iocore/net/QUICMultiCertConfigLoader.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ QUICCertConfig::reconfigure(ConfigContext ctx)
4545
SSLCertLookup *lookup = new SSLCertLookup();
4646

4747
QUICMultiCertConfigLoader loader(params);
48-
auto errata = loader.load(lookup);
48+
auto errata = loader.load(lookup, _config_id == 0);
4949
if (!lookup->is_valid || (errata.has_severity() && errata.severity() >= ERRATA_ERROR)) {
5050
retStatus = false;
5151
}

src/iocore/net/SSLConfig.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@
4646
#include "mgmt/config/ConfigRegistry.h"
4747

4848
#include <openssl/pem.h>
49+
#include <algorithm>
4950
#include <array>
5051
#include <cstring>
5152
#include <cmath>
53+
#include <thread>
5254
#include <unordered_map>
5355

5456
int SSLConfig::config_index = 0;
@@ -125,6 +127,7 @@ SSLConfigParams::reset()
125127
ssl_ctx_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
126128
ssl_client_ctx_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
127129
configExitOnLoadError = 1;
130+
configLoadConcurrency = 1;
128131
}
129132

130133
void
@@ -431,6 +434,10 @@ SSLConfigParams::initialize()
431434

432435
configFilePath = ats_stringdup(RecConfigReadConfigPath("proxy.config.ssl.server.multicert.filename"));
433436
configExitOnLoadError = RecGetRecordInt("proxy.config.ssl.server.multicert.exit_on_load_fail").value_or(0);
437+
configLoadConcurrency = RecGetRecordInt("proxy.config.ssl.server.multicert.concurrency").value_or(1);
438+
if (configLoadConcurrency == 0) {
439+
configLoadConcurrency = std::clamp(static_cast<int>(std::thread::hardware_concurrency()), 1, 256);
440+
}
434441

435442
{
436443
auto rec_str{RecGetRecordStringAlloc("proxy.config.ssl.server.private_key.path")};
@@ -671,7 +678,7 @@ SSLCertificateConfig::reconfigure(ConfigContext ctx)
671678
ink_hrtime_sleep(HRTIME_SECONDS(secs));
672679
}
673680

674-
auto errata = SSLMultiCertConfigLoader(params).load(lookup);
681+
auto errata = SSLMultiCertConfigLoader(params).load(lookup, configid == 0);
675682
if (!lookup->is_valid || (errata.has_severity() && errata.severity() >= ERRATA_ERROR)) {
676683
retStatus = false;
677684
}

src/iocore/net/SSLUtils.cc

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
#include <openssl/ts.h>
7070
#endif
7171

72+
#include <algorithm>
73+
#include <thread>
7274
#include <utility>
7375
#include <string>
7476
#include <unistd.h>
@@ -1599,11 +1601,20 @@ SSLMultiCertConfigLoader::_store_ssl_ctx(SSLCertLookup *lookup, const shared_SSL
15991601
SSLMultiCertConfigLoader::CertLoadData data;
16001602

16011603
if (!this->_prep_ssl_ctx(sslMultCertSettings, data, common_names, unique_names)) {
1602-
lookup->is_valid = false;
1604+
{
1605+
std::lock_guard<std::mutex> lock(_loader_mutex);
1606+
lookup->is_valid = false;
1607+
}
16031608
return false;
16041609
}
16051610

16061611
std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(data, sslMultCertSettings.get());
1612+
1613+
// Serialize all mutations to the shared SSLCertLookup.
1614+
// The expensive work above (_prep_ssl_ctx + init_server_ssl_ctx) runs
1615+
// without the lock, allowing parallel cert loading across threads.
1616+
std::lock_guard<std::mutex> lock(_loader_mutex);
1617+
16071618
for (const auto &loadingctx : ctxs) {
16081619
if (!sslMultCertSettings ||
16091620
!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
17821793
}
17831794

17841795
swoc::Errata
1785-
SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
1796+
SSLMultiCertConfigLoader::load(SSLCertLookup *lookup, bool firstLoad)
17861797
{
17871798
const SSLConfigParams *params = this->_params;
17881799

@@ -1806,10 +1817,69 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
18061817
}
18071818

18081819
swoc::Errata errata(ERRATA_NOTE);
1809-
int item_num = 0;
18101820

1811-
for (const auto &item : parse_result.value) {
1821+
static constexpr int MAX_LOAD_THREADS = 256;
1822+
1823+
int num_threads = params->configLoadConcurrency;
1824+
if (firstLoad) {
1825+
num_threads = std::clamp(static_cast<int>(std::thread::hardware_concurrency()), 1, MAX_LOAD_THREADS);
1826+
}
1827+
num_threads = std::min(num_threads, static_cast<int>(parse_result.value.size()));
1828+
1829+
if (num_threads > 1 && parse_result.value.size() > 1) {
1830+
std::size_t bucket_size = parse_result.value.size() / num_threads;
1831+
std::size_t remainder = parse_result.value.size() % num_threads;
1832+
auto current = parse_result.value.cbegin();
1833+
1834+
std::vector<std::thread> threads;
1835+
Note("(%s) loading %zu certs with %d threads", this->_debug_tag(), parse_result.value.size(), num_threads);
1836+
1837+
for (int t = 0; t < num_threads; ++t) {
1838+
std::size_t this_bucket = bucket_size + (static_cast<std::size_t>(t) < remainder ? 1 : 0);
1839+
auto end = current + this_bucket;
1840+
int base_index = static_cast<int>(std::distance(parse_result.value.cbegin(), current));
1841+
threads.emplace_back(&SSLMultiCertConfigLoader::_load_items, this, lookup, current, end, base_index, std::ref(errata));
1842+
current = end;
1843+
}
1844+
1845+
for (auto &th : threads) {
1846+
th.join();
1847+
}
1848+
1849+
Note("(%s) loaded %zu certs in %d threads", this->_debug_tag(), parse_result.value.size(), num_threads);
1850+
} else {
1851+
_load_items(lookup, parse_result.value.cbegin(), parse_result.value.cend(), 0, errata);
1852+
Note("(%s) loaded %zu certs (single-threaded)", this->_debug_tag(), parse_result.value.size());
1853+
}
1854+
1855+
// We *must* have a default context even if it can't possibly work. The default context is used to
1856+
// bootstrap the SSL handshake so that we can subsequently do the SNI lookup to switch to the real
1857+
// context.
1858+
if (lookup->ssl_default == nullptr) {
1859+
shared_SSLMultiCertConfigParams sslMultiCertSettings(new SSLMultiCertConfigParams);
1860+
sslMultiCertSettings->addr = ats_strdup("*");
1861+
if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
1862+
errata.note(ERRATA_ERROR, "failed set default context");
1863+
}
1864+
}
1865+
1866+
return errata;
1867+
}
1868+
1869+
void
1870+
SSLMultiCertConfigLoader::_load_items(SSLCertLookup *lookup, config::SSLMultiCertConfig::const_iterator begin,
1871+
config::SSLMultiCertConfig::const_iterator end, int base_index, swoc::Errata &errata)
1872+
{
1873+
// Each thread needs its own elevated privileges since POSIX capabilities are per-thread
1874+
uint32_t elevate_setting = 0;
1875+
elevate_setting = RecGetRecordInt("proxy.config.ssl.cert.load_elevated").value_or(0);
1876+
ElevateAccess elevate_access(elevate_setting ? ElevateAccess::FILE_PRIVILEGE : 0);
1877+
1878+
int item_num = base_index;
1879+
for (auto it = begin; it != end; ++it) {
18121880
item_num++;
1881+
const auto &item = *it;
1882+
18131883
shared_SSLMultiCertConfigParams sslMultiCertSettings = std::make_shared<SSLMultiCertConfigParams>();
18141884

18151885
if (!item.ssl_cert_name.empty()) {
@@ -1846,25 +1916,14 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
18461916
// There must be a certificate specified unless the tunnel action is set.
18471917
if (sslMultiCertSettings->cert || sslMultiCertSettings->opt == SSLCertContextOption::OPT_TUNNEL) {
18481918
if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
1919+
std::lock_guard<std::mutex> lock(_loader_mutex);
18491920
errata.note(ERRATA_ERROR, "Failed to load certificate at item {}", item_num);
18501921
}
18511922
} else {
1923+
std::lock_guard<std::mutex> lock(_loader_mutex);
18521924
errata.note(ERRATA_WARN, "No ssl_cert_name specified and no tunnel action set at item {}", item_num);
18531925
}
18541926
}
1855-
1856-
// We *must* have a default context even if it can't possibly work. The default context is used to
1857-
// bootstrap the SSL handshake so that we can subsequently do the SNI lookup to switch to the real
1858-
// context.
1859-
if (lookup->ssl_default == nullptr) {
1860-
shared_SSLMultiCertConfigParams sslMultiCertSettings(new SSLMultiCertConfigParams);
1861-
sslMultiCertSettings->addr = ats_strdup("*");
1862-
if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
1863-
errata.note(ERRATA_ERROR, "failed set default context");
1864-
}
1865-
}
1866-
1867-
return errata;
18681927
}
18691928

18701929
// Release SSL_CTX and the associated data. This works for both

src/records/RecordsConfig.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,9 @@ static constexpr RecordElement RecordsConfig[] =
11821182
{RECT_CONFIG, "proxy.config.ssl.server.multicert.filename", RECD_STRING, ts::filename::SSL_MULTICERT, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
11831183
,
11841184
{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}
1185-
,
1185+
,
1186+
{RECT_CONFIG, "proxy.config.ssl.server.multicert.concurrency", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-256]", RECA_NULL}
1187+
,
11861188
{RECT_CONFIG, "proxy.config.ssl.servername.filename", RECD_STRING, ts::filename::SNI, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
11871189
,
11881190
{RECT_CONFIG, "proxy.config.ssl.server.ticket_key.filename", RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}

tests/gold_tests/tls/ssl_multicert_loader.test.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
ts = Test.MakeATSProcess("ts", enable_tls=True)
2424
server = Test.MakeOriginServer("server")
25-
server2 = Test.MakeOriginServer("server3")
25+
server2 = Test.MakeOriginServer("server2")
2626
request_header = {"headers": f"GET / HTTP/1.1\r\nHost: {sni_domain}\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
2727

2828
response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
@@ -123,3 +123,45 @@
123123
ts2.Disk.traffic_out.Content = Testers.ExcludesExpression(
124124
'Traffic Server is fully initialized', 'process should fail when invalid certificate specified')
125125
ts2.Disk.diags_log.Content = Testers.IncludesExpression('EMERGENCY: failed to load SSL certificate file', 'check diags.log"')
126+
127+
##########################################################################
128+
# Verify parallel cert loading on startup (firstLoad uses hardware_concurrency,
129+
# not the configured concurrency value, so the thread count is host-dependent)
130+
131+
ts3 = Test.MakeATSProcess("ts3", enable_tls=True)
132+
server3 = Test.MakeOriginServer("server3")
133+
server3.addResponse("sessionlog.json", request_header, response_header)
134+
135+
ts3.Disk.records_config.update(
136+
{
137+
'proxy.config.ssl.server.cert.path': f'{ts3.Variables.SSLDir}',
138+
'proxy.config.ssl.server.private_key.path': f'{ts3.Variables.SSLDir}',
139+
})
140+
141+
ts3.addDefaultSSLFiles()
142+
143+
ts3.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server3.Variables.Port}')
144+
145+
# Need at least 2 certs for multi-threading to kick in
146+
ts3.Disk.ssl_multicert_yaml.AddLines(
147+
"""
148+
ssl_multicert:
149+
- dest_ip: "*"
150+
ssl_cert_name: server.pem
151+
ssl_key_name: server.key
152+
- ssl_cert_name: server.pem
153+
ssl_key_name: server.key
154+
""".split("\n"))
155+
156+
tr5 = Test.AddTestRun("Verify parallel cert loading")
157+
tr5.Processes.Default.StartBefore(ts3)
158+
tr5.Processes.Default.StartBefore(server3)
159+
tr5.StillRunningAfter = ts3
160+
tr5.StillRunningAfter = server3
161+
tr5.MakeCurlCommand(
162+
f"-q -s -v -k --resolve '{sni_domain}:{ts3.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts3.Variables.ssl_port}",
163+
ts=ts3)
164+
tr5.Processes.Default.ReturnCode = 0
165+
tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response")
166+
tr5.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response")
167+
ts3.Disk.diags_log.Content = Testers.IncludesExpression('loaded 2 certs', 'verify certs were loaded successfully')

0 commit comments

Comments
 (0)