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

bneradt 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 01ed4876a4 Refresh default TLS context on secret update (#13342)
01ed4876a4 is described below

commit 01ed4876a4e433d28ef8419a0759df7cfcab0b47
Author: Brian Neradt <[email protected]>
AuthorDate: Mon Jul 13 16:15:49 2026 -0500

    Refresh default TLS context on secret update (#13342)
    
    Default server certificate secret updates could rebuild the TLS
    contexts for CN/SAN lookups while leaving the default/no-SNI context
    pointing at the old SSL_CTX. Operators could update cert material on
    disk and through the secret API, but new handshakes without a more
    specific match could still serve the stale certificate.
    
    This updates runtime context refresh to cover address/default lookup
    entries owned by the same ssl_multicert policy and retains the
    default context while callers create new TLS sessions. This also adds
    an AuTest that updates a plugin-loaded default certificate and
    verifies the next no-SNI handshake sees the new certificate.
    
    Fixes: #9562
---
 src/iocore/net/P_SSLCertLookup.h                   |  21 +-
 src/iocore/net/QUICPacketHandler.cc                |   3 +-
 src/iocore/net/SSLCertLookup.cc                    |  46 ++--
 src/iocore/net/SSLNetVConnection.cc                |   9 +-
 src/iocore/net/SSLStats.cc                         |   5 +-
 src/iocore/net/SSLUtils.cc                         |  27 ++-
 .../tls/tls_secret_update_default.test.py          | 234 +++++++++++++++++++++
 tests/tools/plugins/ssl_secret_load_test.cc        |   8 +-
 8 files changed, 320 insertions(+), 33 deletions(-)

diff --git a/src/iocore/net/P_SSLCertLookup.h b/src/iocore/net/P_SSLCertLookup.h
index 5d13a6300f..6367818ece 100644
--- a/src/iocore/net/P_SSLCertLookup.h
+++ b/src/iocore/net/P_SSLCertLookup.h
@@ -26,6 +26,8 @@
 #include "iocore/eventsystem/ConfigProcessor.h"
 #include "iocore/net/SSLTypes.h"
 #include "records/RecCore.h"
+#include "tsutil/Bravo.h"
+#include <mutex>
 #include <shared_mutex>
 
 #include <set>
@@ -134,8 +136,7 @@ struct SSLCertLookup : public ConfigInfo {
   std::unique_ptr<SSLContextStorage> ssl_storage;
   std::unique_ptr<SSLContextStorage> ec_storage;
 
-  shared_SSL_CTX ssl_default;
-  bool           is_valid = true;
+  bool is_valid = true;
 
   int insert(const char *name, SSLCertContext const &cc);
   int insert(const IpEndpoint &address, SSLCertContext const &cc);
@@ -146,6 +147,7 @@ struct SSLCertLookup : public ConfigInfo {
       @return @c A pointer to the matched context, @c nullptr if no match is 
found.
   */
   SSLCertContext *find(const IpEndpoint &address) const;
+  SSLCertContext *find(const IpEndpoint &address, [[maybe_unused]] 
SSLCertContextType ctxType) const;
 
   /** Find certificate context by name (FQDN).
       Exact matches have priority, then wildcards. Only destination based 
matches are checked.
@@ -154,10 +156,18 @@ struct SSLCertLookup : public ConfigInfo {
   SSLCertContext *find(const std::string &name, SSLCertContextType ctxType = 
SSLCertContextType::GENERIC) const;
 
   // Return the last-resort default TLS context if there is no name or address 
match.
-  SSL_CTX *
+  shared_SSL_CTX
   defaultContext() const
   {
-    return ssl_default.get();
+    ts::bravo::shared_lock<ts::bravo::shared_mutex> lock(default_ctx_mutex);
+    return ssl_default;
+  }
+
+  void
+  setDefaultContext(shared_SSL_CTX ctx) const
+  {
+    std::lock_guard<ts::bravo::shared_mutex> lock(default_ctx_mutex);
+    ssl_default = std::move(ctx);
   }
 
   unsigned        count(SSLCertContextType ctxType = 
SSLCertContextType::GENERIC) const;
@@ -170,6 +180,9 @@ struct SSLCertLookup : public ConfigInfo {
   ~SSLCertLookup() override;
 
 private:
+  mutable ts::bravo::shared_mutex default_ctx_mutex;
+  mutable shared_SSL_CTX          ssl_default;
+
   // Map cert_secret name to lookup keys
   std::unordered_map<std::string, std::vector<std::string>> 
cert_secret_registry;
 };
diff --git a/src/iocore/net/QUICPacketHandler.cc 
b/src/iocore/net/QUICPacketHandler.cc
index d8650c858d..406c9ef720 100644
--- a/src/iocore/net/QUICPacketHandler.cc
+++ b/src/iocore/net/QUICPacketHandler.cc
@@ -281,7 +281,8 @@ QUICPacketHandlerIn::_recv_packet(int /* event ATS_UNUSED 
*/, UDPPacket *udp_pac
     QUICConnectionId new_cid;
 
     QUICCertConfig::scoped_config server_cert;
-    SSL                          *ssl = SSL_new(server_cert->defaultContext());
+    auto                          default_ctx = server_cert->defaultContext();
+    SSL                          *ssl         = SSL_new(default_ctx.get());
 
     quiche_conn *quiche_con = quiche_conn_new_with_tls(
       new_cid, new_cid.length(), retry_token.original_dcid(), 
retry_token.original_dcid().length(), &udp_packet->to.sa,
diff --git a/src/iocore/net/SSLCertLookup.cc b/src/iocore/net/SSLCertLookup.cc
index 4b51a21579..bf33d0294f 100644
--- a/src/iocore/net/SSLCertLookup.cc
+++ b/src/iocore/net/SSLCertLookup.cc
@@ -273,8 +273,8 @@ SSLCertContext::setCtx(shared_SSL_CTX sc)
 SSLCertLookup::SSLCertLookup()
   : ssl_storage(std::make_unique<SSLContextStorage>()),
     ec_storage(std::make_unique<SSLContextStorage>()),
-    ssl_default(nullptr),
-    is_valid(true)
+    is_valid(true),
+    ssl_default(nullptr)
 {
 }
 
@@ -299,36 +299,48 @@ SSLCertLookup::find(const std::string &address, 
[[maybe_unused]] SSLCertContextT
 SSLCertContext *
 SSLCertLookup::find(const IpEndpoint &address) const
 {
-  SSLCertContext     *cc;
-  SSLAddressLookupKey key(address);
-
 #ifdef OPENSSL_IS_BORINGSSL
   // If the context is EC supportable, try finding that first.
-  if ((cc = this->ec_storage->lookup(key.get()))) {
+  if (auto *cc = this->find(address, SSLCertContextType::EC)) {
     return cc;
   }
+#endif
 
-  // If that failed, try the address without the port.
-  if (address.network_order_port()) {
-    key.split();
-    if ((cc = this->ec_storage->lookup(key.get()))) {
-      return cc;
-    }
-  }
+  return this->find(address, SSLCertContextType::RSA);
+}
+
+SSLCertContext *
+SSLCertLookup::find(const IpEndpoint &address, [[maybe_unused]] 
SSLCertContextType ctxType) const
+{
+  SSLAddressLookupKey key(address);
 
-  // reset for search across RSA
-  key = SSLAddressLookupKey(address);
+#ifdef OPENSSL_IS_BORINGSSL
+  SSLContextStorage *storage = nullptr;
+  switch (ctxType) {
+  case SSLCertContextType::GENERIC:
+  case SSLCertContextType::RSA:
+    storage = ssl_storage.get();
+    break;
+  case SSLCertContextType::EC:
+    storage = ec_storage.get();
+    break;
+  default:
+    ink_assert(false);
+    return nullptr;
+  }
+#else
+  SSLContextStorage *storage = ssl_storage.get();
 #endif
 
   // First try the full address.
-  if ((cc = this->ssl_storage->lookup(key.get()))) {
+  if (auto *cc = storage->lookup(key.get())) {
     return cc;
   }
 
   // If that failed, try the address without the port.
   if (address.network_order_port()) {
     key.split();
-    return this->ssl_storage->lookup(key.get());
+    return storage->lookup(key.get());
   }
 
   return nullptr;
diff --git a/src/iocore/net/SSLNetVConnection.cc 
b/src/iocore/net/SSLNetVConnection.cc
index f7ecdd9142..2a2ac30836 100644
--- a/src/iocore/net/SSLNetVConnection.cc
+++ b/src/iocore/net/SSLNetVConnection.cc
@@ -1119,7 +1119,8 @@ SSLNetVConnection::_sslStartHandShake(int event, int &err)
         }
         ats_ip_nptop(&dst, ipb1, sizeof(ipb1));
         ats_ip_nptop(&src, ipb2, sizeof(ipb2));
-        DbgPrint(dbg_ctl_ssl, "IP context is %p for [%s] -> [%s], default 
context %p", cc, ipb2, ipb1, lookup->defaultContext());
+        auto default_ctx = lookup->defaultContext();
+        DbgPrint(dbg_ctl_ssl, "IP context is %p for [%s] -> [%s], default 
context %p", cc, ipb2, ipb1, default_ctx.get());
       }
 
       // Escape if this is marked to be a tunnel.
@@ -1141,7 +1142,7 @@ SSLNetVConnection::_sslStartHandShake(int event, int &err)
       // Attach the default SSL_CTX to this SSL session. The default context 
is never going to be able
       // to negotiate a SSL session, but it's enough to trampoline us into the 
SNI callback where we
       // can select the right server certificate.
-      this->_make_ssl_connection(lookup->defaultContext());
+      this->_make_ssl_connection(lookup->defaultContext().get());
     }
 
     if (this->ssl == nullptr) {
@@ -2020,8 +2021,8 @@ SSLNetVConnection::_lookupContextByIP()
         return nullptr;
       }
       ats_ip_nptop(&src, ipb2, sizeof(ipb2));
-      DbgPrint(dbg_ctl_proxyprotocol, "IP context is %p for [%s] -> [%s], 
default context %p", cc, ipb2, ipb1,
-               lookup->defaultContext());
+      auto default_ctx = lookup->defaultContext();
+      DbgPrint(dbg_ctl_proxyprotocol, "IP context is %p for [%s] -> [%s], 
default context %p", cc, ipb2, ipb1, default_ctx.get());
     }
   } else if (0 == safe_getsockname(this->get_socket(), &ip.sa, &namelen)) {
     cc = lookup->find(ip);
diff --git a/src/iocore/net/SSLStats.cc b/src/iocore/net/SSLStats.cc
index 2ca32677f0..514c2e68eb 100644
--- a/src/iocore/net/SSLStats.cc
+++ b/src/iocore/net/SSLStats.cc
@@ -278,12 +278,13 @@ SSLInitializeStatistics()
   // Acquire the loaded SSL certificate configuration to enumerate ciphers and 
groups.
   // This must be called AFTER SSLCertificateConfig::startup().
   SSLCertificateConfig::scoped_config lookup;
-  if (!lookup || !lookup->ssl_default) {
+  auto                                default_ctx = lookup ? 
lookup->defaultContext() : nullptr;
+  if (!default_ctx) {
     Dbg(dbg_ctl_ssl, "No SSL configuration, skipping cipher/group statistics 
initialization");
     return;
   }
 
-  SSL_CTX *ctx                  = lookup->ssl_default.get();
+  SSL_CTX *ctx                  = default_ctx.get();
   SSL     *ssl                  = SSL_new(ctx);
   STACK_OF(SSL_CIPHER) *ciphers = SSL_get_ciphers(ssl);
 
diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc
index 7c7637affa..e09df82dfe 100644
--- a/src/iocore/net/SSLUtils.cc
+++ b/src/iocore/net/SSLUtils.cc
@@ -1742,6 +1742,27 @@ SSLMultiCertConfigLoader::update_ssl_ctx(const 
std::string &secret_name)
       if (!ctx) {
         retval = false;
       } else {
+        if ((*policy_iter)->addr) {
+          SSLCertContext *cc = nullptr;
+          if (strcmp((*policy_iter)->addr, "*") == 0) {
+            this->_set_handshake_callbacks(ctx.get());
+            cc = lookup->find("*", loadingctx.ctx_type);
+          } else {
+            IpEndpoint ep;
+            if (ats_ip_pton((*policy_iter)->addr, &ep) == 0) {
+              cc = lookup->find(ep, loadingctx.ctx_type);
+            } else {
+              Error("'%s' is not a valid IPv4 or IPv6 address", (const char 
*)(*policy_iter)->addr);
+              retval = false;
+            }
+          }
+          if (cc && cc->userconfig.get() == policy_iter->get()) {
+            cc->setCtx(ctx);
+            if (strcmp((*policy_iter)->addr, "*") == 0) {
+              lookup->setDefaultContext(ctx);
+            }
+          }
+        }
         for (auto const &name : common_names) {
           SSLCertContext *cc = lookup->find(name, loadingctx.ctx_type);
           if (cc && cc->userconfig.get() == policy_iter->get()) {
@@ -1796,8 +1817,8 @@ 
SSLMultiCertConfigLoader::_store_single_ssl_ctx(SSLCertLookup *lookup, const sha
     if (strcmp(sslMultCertSettings->addr, "*") == 0) {
       Dbg(dbg_ctl_ssl_load, "Addr is '*'; setting %p to default", ctx.get());
       if (lookup->insert(sslMultCertSettings->addr, SSLCertContext(ctx, 
ctx_type, sslMultCertSettings, keyblock)) >= 0) {
-        inserted            = true;
-        lookup->ssl_default = ctx;
+        inserted = true;
+        lookup->setDefaultContext(ctx);
         this->_set_handshake_callbacks(ctx.get());
       }
     } else {
@@ -1899,7 +1920,7 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup, 
bool firstLoad)
   // 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) {
+  if (lookup->defaultContext() == nullptr) {
     shared_SSLMultiCertConfigParams sslMultiCertSettings(new 
SSLMultiCertConfigParams);
     sslMultiCertSettings->addr = ats_strdup("*");
     if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
diff --git a/tests/gold_tests/tls/tls_secret_update_default.test.py 
b/tests/gold_tests/tls/tls_secret_update_default.test.py
new file mode 100644
index 0000000000..ae530c70ad
--- /dev/null
+++ b/tests/gold_tests/tls/tls_secret_update_default.test.py
@@ -0,0 +1,234 @@
+'''
+Test default server certificate updates through 
TSSslSecretSet/TSSslSecretUpdate.
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you 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.
+
+import os
+import re
+
+Test.Summary = '''
+Verify that secret updates refresh the default server SSL_CTX used without SNI.
+'''
+
+
+class TestDefaultSecretUpdate:
+    '''Verify default server certificate updates through the secret API.'''
+
+    initial_cert: str = 'signed-bar.pem'
+    updated_cert: str = 'signed2-bar.pem'
+    key_file: str = 'signed-bar.key'
+    shadowed_cert: str = 'signed-foo.pem'
+    updated_shadowed_cert: str = 'signed2-foo.pem'
+    shadowed_key_file: str = 'signed-foo.key'
+    plugin: str = 'ssl_secret_load_test.so'
+
+    def __init__(self) -> None:
+        '''Configure the ATS process, origin server, and test runs.'''
+        self._configure_server()
+        self._configure_traffic_server()
+        self._add_start_run()
+        self._add_config_touch_run()
+        self._add_config_reload_run()
+        self._add_config_reload_wait_run()
+        self._add_initial_certificate_run()
+        self._add_mtime_delay_run()
+        self._add_shadowed_certificate_update_run()
+        self._add_secret_update_wait_run(self.shadowed_cert)
+        self._add_unchanged_default_certificate_run()
+        self._add_default_certificate_update_run()
+        self._add_secret_update_wait_run(self.initial_cert)
+        self._add_updated_certificate_run()
+
+    def _configure_server(self) -> 'Process':
+        '''Configure the origin server.'''
+        server = Test.MakeOriginServer('server')
+        self._server = server
+
+        request_header = {'headers': 'GET / HTTP/1.1\r\nHost: 
doesnotmatter\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': ''}
+        server.addResponse('sessionlog.json', request_header, response_header)
+        return server
+
+    def _configure_traffic_server(self) -> 'Process':
+        '''Configure the Traffic Server process.'''
+        ts = Test.MakeATSProcess('ts', enable_tls=True)
+        self._ts = ts
+
+        ts.addSSLfile(f'ssl/{self.initial_cert}')
+        ts.addSSLfile(f'ssl/{self.updated_cert}')
+        ts.addSSLfile(f'ssl/{self.key_file}')
+        ts.addSSLfile(f'ssl/{self.shadowed_cert}')
+        ts.addSSLfile(f'ssl/{self.updated_shadowed_cert}')
+        ts.addSSLfile(f'ssl/{self.shadowed_key_file}')
+        Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 
self.plugin), ts)
+
+        ts.Disk.records_config.update(
+            {
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'ssl_secret_load_test',
+                'proxy.config.ssl.server.cert.path': 
f'{ts.Variables.SSLDir}/../',
+                'proxy.config.ssl.server.private_key.path': 
f'{ts.Variables.SSLDir}/../',
+                'proxy.config.ssl.server.multicert.concurrency': 1,
+                'proxy.config.exec_thread.autoconfig.scale': 1.0,
+                'proxy.config.url_remap.pristine_host_hdr': 1,
+            })
+
+        ts.Disk.ssl_multicert_yaml.AddLines(
+            [
+                'ssl_multicert:',
+                '  - dest_ip: "*"',
+                f'    ssl_cert_name: {self.initial_cert}',
+                f'    ssl_key_name: {self.key_file}',
+                '  - dest_ip: "*"',
+                f'    ssl_cert_name: {self.shadowed_cert}',
+                f'    ssl_key_name: {self.shadowed_key_file}',
+            ])
+        ts.Disk.remap_config.AddLine(f'map / 
http://127.0.0.1:{self._server.Variables.Port}')
+        return ts
+
+    def _add_start_run(self) -> 'TestRun':
+        '''Start ATS before reloading the multicert configuration serially.'''
+        tr = Test.AddTestRun('Start ATS')
+        tr.Processes.Default.Command = 'echo starting ATS'
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_config_reload_run(self) -> 'TestRun':
+        '''Reload the multicert configuration with deterministic ordering.'''
+        tr = Test.AddTestRun('Reload the multicert configuration serially')
+        tr.Processes.Default.Command = 'traffic_ctl config reload'
+        tr.Processes.Default.Env = self._ts.Env
+        tr.Processes.Default.ReturnCode = 0
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_config_touch_run(self) -> 'TestRun':
+        '''Mark the multicert configuration for reload.'''
+        tr = Test.AddTestRun('Mark the multicert configuration for reload')
+        tr.Processes.Default.Command = f'touch 
{self._ts.Disk.ssl_multicert_yaml.AbsPath}'
+        tr.Processes.Default.ReturnCode = 0
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_config_reload_wait_run(self) -> 'TestRun':
+        '''Wait for the serial multicert configuration reload to finish.'''
+        tr = Test.AddTestRun('Wait for the multicert configuration reload')
+        tr.Processes.Default.Command = 'echo awaiting multicert reload'
+        tr.Processes.Default.Env = self._ts.Env
+        tr.Processes.Default.ReturnCode = 0
+        await_reload = tr.Processes.Process('await_reload', 'sleep 30')
+        await_reload.Ready = When.FileContains(self._ts.Disk.diags_log.Name, 
'ssl_multicert.yaml finished loading', 2)
+        tr.Processes.Default.StartBefore(await_reload)
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_initial_certificate_run(self) -> 'TestRun':
+        '''Verify the original default certificate before the secret update.'''
+        tr = self._add_curl_run(
+            'Initial default certificate',
+            expected_cn='signer.yahoo.com',
+            unexpected_cn='signer2.yahoo.com',
+            expected_description='Initial cert uses signer.',
+            unexpected_description='Initial cert is not updated yet.')
+        tr.Processes.Default.Streams.All += 
Testers.ContainsExpression(r'subject:.*CN=bar\.com', 'Default cert is bar.com.')
+        tr.Processes.Default.Streams.All += 
Testers.ExcludesExpression(r'subject:.*CN=foo\.com', 'Shadowed cert is not 
served.')
+        return tr
+
+    def _add_mtime_delay_run(self) -> 'TestRun':
+        '''Make the replacement certificate mtime differ from the original.'''
+        tr = Test.AddTestRun('Make the cert mtime differ')
+        tr.Processes.Default.Command = 'sleep 2'
+        tr.Processes.Default.ReturnCode = 0
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_shadowed_certificate_update_run(self) -> 'TestRun':
+        '''Replace the certificate for the shadowed wildcard policy.'''
+        tr = Test.AddTestRun('Update the shadowed wildcard certificate')
+        tr.Setup.CopyAs(f'ssl/{self.updated_shadowed_cert}', '.', 
f'{self._ts.Variables.SSLDir}/{self.shadowed_cert}')
+        tr.Processes.Default.Command = f'touch 
{self._ts.Variables.SSLDir}/{self.shadowed_cert}'
+        tr.Processes.Default.ReturnCode = 0
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_unchanged_default_certificate_run(self) -> 'TestRun':
+        '''Verify that refreshing a shadowed wildcard does not replace the 
default.'''
+        tr = self._add_curl_run(
+            'Default certificate unchanged after shadowed wildcard update',
+            expected_cn='signer.yahoo.com',
+            unexpected_cn='signer2.yahoo.com',
+            expected_description='Default cert still uses signer.',
+            unexpected_description='Shadowed wildcard cert did not become the 
default.')
+        tr.Processes.Default.Streams.All += 
Testers.ContainsExpression(r'subject:.*CN=bar\.com', 'Default cert is still 
bar.com.')
+        tr.Processes.Default.Streams.All += Testers.ExcludesExpression(
+            r'subject:.*CN=foo\.com', 'Shadowed foo.com cert is not served.')
+        return tr
+
+    def _add_default_certificate_update_run(self) -> 'TestRun':
+        '''Replace the plugin backing default certificate file.'''
+        tr = Test.AddTestRun('Update the default certificate')
+        tr.Setup.CopyAs(f'ssl/{self.updated_cert}', '.', 
f'{self._ts.Variables.SSLDir}/{self.initial_cert}')
+        tr.Processes.Default.Command = f'touch 
{self._ts.Variables.SSLDir}/{self.initial_cert}'
+        tr.Processes.Default.ReturnCode = 0
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_secret_update_wait_run(self, secret_name: str) -> 'TestRun':
+        '''Wait for the test plugin to update the certificate secret.'''
+        tr = Test.AddTestRun(f'Wait for the {secret_name} secret update')
+        tr.Processes.Default.Command = 'echo awaiting secret update'
+        tr.Processes.Default.ReturnCode = 0
+        await_update = tr.Processes.Process('await_update', 'sleep 30')
+        await_update.Ready = When.FileContains(
+            self._ts.Disk.traffic_out.Name, f'updated cert for secret 
.*{re.escape(secret_name)}')
+        tr.Processes.Default.StartBefore(await_update)
+        self._keep_processes_running(tr)
+        return tr
+
+    def _add_updated_certificate_run(self) -> 'TestRun':
+        '''Verify the updated default certificate after the secret update.'''
+        return self._add_curl_run(
+            'Updated default certificate',
+            expected_cn='signer2.yahoo.com',
+            unexpected_cn='signer.yahoo.com',
+            expected_description='Updated cert uses signer2.',
+            unexpected_description='Updated cert no longer uses signer.')
+
+    def _add_curl_run(
+            self, name: str, expected_cn: str, unexpected_cn: str, 
expected_description: str,
+            unexpected_description: str) -> 'TestRun':
+        '''Add a curl run against ATS without SNI.'''
+        tr = Test.AddTestRun(name)
+        self._keep_processes_running(tr)
+        tr.MakeCurlCommand(
+            f"-k -v --http1.1 -H 'host: doesnotmatter' 
https://127.0.0.1:{self._ts.Variables.ssl_port}/";, ts=self._ts)
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.All = 
Testers.ContainsExpression(f'issuer:.*CN={re.escape(expected_cn)}', 
expected_description)
+        tr.Processes.Default.Streams.All += Testers.ExcludesExpression(
+            f'issuer:.*CN={re.escape(unexpected_cn)}', unexpected_description)
+        return tr
+
+    def _keep_processes_running(self, tr: 'TestRun') -> None:
+        '''Keep ATS and the origin server running after a TestRun.'''
+        tr.StillRunningAfter = self._ts
+        tr.StillRunningAfter = self._server
+
+
+TestDefaultSecretUpdate()
diff --git a/tests/tools/plugins/ssl_secret_load_test.cc 
b/tests/tools/plugins/ssl_secret_load_test.cc
index 54c26eeb0a..b9f1dfc656 100644
--- a/tests/tools/plugins/ssl_secret_load_test.cc
+++ b/tests/tools/plugins/ssl_secret_load_test.cc
@@ -159,8 +159,12 @@ CB_Update_Secret(TSCont cont, TSEvent /* event ATS_UNUSED 
*/, void * /* edata AT
     }
   }
   for (auto name : updates) {
-    Dbg(dbg_ctl, "update cert for secret %s", name.c_str());
-    TSSslSecretUpdate(name.c_str(), name.length());
+    TSReturnCode result = TSSslSecretUpdate(name.c_str(), name.length());
+    if (result == TS_SUCCESS) {
+      Dbg(dbg_ctl, "updated cert for secret %s", name.c_str());
+    } else {
+      TSError("[%s] failed to update cert for secret %s", PN, name.c_str());
+    }
   }
   TSContScheduleOnPool(cont, 3000, TS_THREAD_POOL_TASK);
   return TS_SUCCESS;

Reply via email to