You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2021/10/07 15:05:41 UTC

[trafficserver] branch 9.2.x updated: Adding TLS session key logging capability (#8337)

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

zwoop pushed a commit to branch 9.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/9.2.x by this push:
     new f18e749  Adding TLS session key logging capability (#8337)
f18e749 is described below

commit f18e7497b5929a103ff21f2d87de06d55a2a8de6
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Wed Oct 6 13:00:59 2021 -0500

    Adding TLS session key logging capability (#8337)
    
    Adding the ability to log TLS session keys to a log file for packet
    capture decryption purposes.
    
    This adds the following reloadable configuration:
    proxy.config.ssl.keylog_file
    
    Since this can work for QUIC as well, this also deprecates:
    proxy.config.quic.client.keylog_file
    
    (cherry picked from commit b26795d307024570eda96c30aaf96a8af4e85bde)
---
 build/crypto.m4                                    |  15 +++
 configure.ac                                       |   3 +
 doc/admin-guide/files/records.config.en.rst        |  18 ++--
 include/tscore/ink_config.h.in                     |   1 +
 iocore/net/P_SSLConfig.h                           |   2 +
 iocore/net/P_SSLUtils.h                            | 104 ++++++++++++++++++++-
 iocore/net/QUICNetVConnection.cc                   |   4 +-
 iocore/net/QUICPacketHandler.cc                    |   2 +-
 iocore/net/SSLClientCoordinator.cc                 |   1 +
 iocore/net/SSLClientUtils.cc                       |   7 ++
 iocore/net/SSLConfig.cc                            |  10 ++
 iocore/net/SSLUtils.cc                             |  87 +++++++++++++++++
 iocore/net/quic/QUICConfig.cc                      |  12 +--
 iocore/net/quic/QUICConfig.h                       |   2 -
 iocore/net/quic/QUICGlobals.cc                     |  17 ----
 iocore/net/quic/QUICTLS.cc                         |   6 --
 iocore/net/quic/QUICTLS.h                          |   4 +-
 iocore/net/quic/QUICTLS_boringssl.cc               |   8 +-
 iocore/net/quic/QUICTLS_openssl.cc                 |   8 +-
 lib/perl/lib/Apache/TS/AdminClient.pm              |   1 +
 mgmt/RecordsConfig.cc                              |   4 +-
 .../tls/tls_session_key_logging.replay.yaml        |  48 ++++++++++
 .../gold_tests/tls/tls_session_key_logging.test.py |  96 +++++++++++++++++++
 23 files changed, 399 insertions(+), 61 deletions(-)

diff --git a/build/crypto.m4 b/build/crypto.m4
index b7bb9c6..f804c94 100644
--- a/build/crypto.m4
+++ b/build/crypto.m4
@@ -274,6 +274,21 @@ AC_DEFUN([TS_CHECK_CRYPTO_OCSP], [
 dnl
 dnl Since OpenSSL 1.1.1
 dnl
+AC_DEFUN([TS_CHECK_CRYPTO_KEYLOGGING], [
+  _keylogging_saved_LIBS=$LIBS
+  TS_ADDTO(LIBS, [$OPENSSL_LIBS])
+  AC_CHECK_FUNCS(SSL_CTX_set_keylog_callback, [enable_tls_keylogging=yes], [enable_tls_keylogging=no])
+  LIBS=$_keylogging_saved_LIBS
+
+  AC_MSG_CHECKING(whether to enable TLS keylogging support)
+  AC_MSG_RESULT([$enable_tls_keylogging])
+  TS_ARG_ENABLE_VAR([has], [tls-keylogging])
+  AC_SUBST(has_tls_keylogging)
+])
+
+dnl
+dnl Since OpenSSL 1.1.1
+dnl
 AC_DEFUN([TS_CHECK_CRYPTO_SET_CIPHERSUITES], [
   _set_ciphersuites_saved_LIBS=$LIBS
 
diff --git a/configure.ac b/configure.ac
index 8d0a340..1cf7d1e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1286,6 +1286,9 @@ TS_CHECK_CRYPTO_OCSP
 # Check for SSL_CTX_set_ciphersuites call
 TS_CHECK_CRYPTO_SET_CIPHERSUITES
 
+# Check for TOLS keylogging support.
+TS_CHECK_CRYPTO_KEYLOGGING
+
 # Check for openssl early data support
 TS_CHECK_EARLY_DATA
 
diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index b73c834..26106a3 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -3775,6 +3775,18 @@ SSL Termination
 
    See :ref:`admin-performance-timeouts` for more discussion on |TS| timeouts.
 
+.. ts:cv:: CONFIG proxy.config.ssl.keylog_file STRING NULL
+   :reloadable:
+
+   If configured, TLS session keys for TLS connections will be logged to the
+   specified file. This file is formatted in such a way that it can be
+   conveniently imported into tools such as Wireshark to decrypt packet
+   captures.  This should only be used for debugging purposes since the data in
+   the keylog file can be used to decrypt the otherwise encrypted traffic. A
+   NULL value for this disables the feature.
+
+   This feature is disabled by default.
+
 Client-Related Configuration
 ----------------------------
 
@@ -4234,12 +4246,6 @@ removed in the future without prior notice.
    If specified, TLS session data will be stored to the file, and will be used
    for resuming a session.
 
-.. ts:cv:: CONFIG proxy.config.quic.client.keylog_file STRING ""
-   :reloadable:
-
-   Only available for :program:`traffic_quic`.
-   If specified, key information will be stored to the file.
-
 .. ts:cv:: CONFIG proxy.config.quic.no_activity_timeout_in INT 30000
    :reloadable:
 
diff --git a/include/tscore/ink_config.h.in b/include/tscore/ink_config.h.in
index 0c97a62..d66e509 100644
--- a/include/tscore/ink_config.h.in
+++ b/include/tscore/ink_config.h.in
@@ -75,6 +75,7 @@
 #define TS_USE_TLS13 @use_tls13@
 #define TS_USE_QUIC @use_quic@
 #define TS_USE_TLS_SET_CIPHERSUITES @use_tls_set_ciphersuites@
+#define TS_HAS_TLS_KEYLOGGING @has_tls_keylogging@
 #define TS_USE_LINUX_NATIVE_AIO @use_linux_native_aio@
 #define TS_USE_REMOTE_UNWINDING @use_remote_unwinding@
 #define TS_USE_TLS_OCSP @use_tls_ocsp@
diff --git a/iocore/net/P_SSLConfig.h b/iocore/net/P_SSLConfig.h
index 262df7c..8e92525 100644
--- a/iocore/net/P_SSLConfig.h
+++ b/iocore/net/P_SSLConfig.h
@@ -106,6 +106,8 @@ struct SSLConfigParams : public ConfigInfo {
   char *server_groups_list;
   char *client_groups_list;
 
+  char *keylog_file;
+
   static uint32_t server_max_early_data;
   static uint32_t server_recv_max_early_data;
   static bool server_allow_early_data_params;
diff --git a/iocore/net/P_SSLUtils.h b/iocore/net/P_SSLUtils.h
index b01da6f..4215644 100644
--- a/iocore/net/P_SSLUtils.h
+++ b/iocore/net/P_SSLUtils.h
@@ -34,8 +34,10 @@
 #include "records/I_RecCore.h"
 #include "P_SSLCertLookup.h"
 
-#include <set>
 #include <map>
+#include <mutex>
+#include <set>
+#include <shared_mutex>
 
 struct SSLConfigParams;
 class SSLNetVConnection;
@@ -60,6 +62,105 @@ struct SSLLoadingContext {
   explicit SSLLoadingContext(SSL_CTX *c, SSLCertContextType ctx_type) : ctx(c), ctx_type(ctx_type) {}
 };
 
+/** A class for handling TLS secrets logging. */
+class TLSKeyLogger
+{
+public:
+  TLSKeyLogger(const TLSKeyLogger &) = delete;
+  TLSKeyLogger &operator=(const TLSKeyLogger &) = delete;
+
+  ~TLSKeyLogger()
+  {
+    std::unique_lock lock{_mutex};
+    close_keylog_file();
+  }
+
+  /** A callback for TLS secret key logging.
+   *
+   * This is the callback registered with OpenSSL's SSL_CTX_set_keylog_callback
+   * to log TLS secrets if the user enabled that feature. For more information
+   * about this callback, see OpenSSL's documentation of
+   * SSL_CTX_set_keylog_callback.
+   *
+   * @param[in] ssl The SSL object associated with the connection.
+   * @param[in] line The line to place in the keylog file.
+   */
+  static void
+  ssl_keylog_cb(const SSL *ssl, const char *line)
+  {
+    instance().log(line);
+  }
+
+  /** Return whether TLS key logging is enabled.
+   *
+   * @return True if TLS session key logging is enabled, false otherwise.
+   */
+  static bool
+  is_enabled()
+  {
+    return instance()._fd >= 0;
+  }
+
+  /** Enable keylogging.
+   *
+   * @param[in] keylog_file The path to the file to log TLS secrets to.
+   */
+  static void
+  enable_keylogging(const char *keylog_file)
+  {
+    instance().enable_keylogging_internal(keylog_file);
+  }
+
+  /** Disable TLS secrets logging. */
+  static void
+  disable_keylogging()
+  {
+    instance().disable_keylogging_internal();
+  }
+
+private:
+  TLSKeyLogger() = default;
+
+  /** Return the TLSKeyLogger singleton.
+   *
+   * We use a getter rather than a class static singleton member so that the
+   * construction of the singleton delayed until after TLS configuration is
+   * processed.
+   */
+  static TLSKeyLogger &
+  instance()
+  {
+    static TLSKeyLogger instance;
+    return instance;
+  }
+
+  /** Close the file descriptor for the key log file.
+   *
+   * @note This assumes that a unique lock has been acquired for _mutex.
+   */
+  void close_keylog_file();
+
+  /** A TLS secret line to log to the keylog file.
+   *
+   * @param[in] line A line to log to the keylog file.
+   */
+  void log(const char *line);
+
+  /** Enable TLS keylogging in the instance singleton. */
+  void enable_keylogging_internal(const char *keylog_file);
+
+  /** Disable TLS keylogging in the instance singleton. */
+  void disable_keylogging_internal();
+
+private:
+  /** A file descriptor for the log file receiving the TLS secrets. */
+  int _fd = -1;
+
+  /** A mutex to coordinate dynamically changing TLS logging config changes and
+   * logging to the TLS log file. */
+  std::shared_mutex _mutex;
+};
+
 /**
     @brief Load SSL certificates from ssl_multicert.config and setup SSLCertLookup for SSLCertificateConfig
  */
@@ -122,6 +223,7 @@ private:
   virtual bool _set_info_callback(SSL_CTX *ctx);
   virtual bool _set_npn_callback(SSL_CTX *ctx);
   virtual bool _set_alpn_callback(SSL_CTX *ctx);
+  virtual bool _set_keylog_callback(SSL_CTX *ctx);
 };
 
 // Create a new SSL server context fully configured (cert and keys are optional).
diff --git a/iocore/net/QUICNetVConnection.cc b/iocore/net/QUICNetVConnection.cc
index d2d07fd..979fc86 100644
--- a/iocore/net/QUICNetVConnection.cc
+++ b/iocore/net/QUICNetVConnection.cc
@@ -2323,8 +2323,8 @@ QUICNetVConnection::_setup_handshake_protocol(const shared_SSL_CTX &ctx)
 {
   // Initialize handshake protocol specific stuff
   // For QUICv1 TLS is the only option
-  QUICTLS *tls = new QUICTLS(this->_pp_key_info, ctx.get(), this->direction(), this->options,
-                             this->_quic_config->client_session_file(), this->_quic_config->client_keylog_file());
+  QUICTLS *tls =
+    new QUICTLS(this->_pp_key_info, ctx.get(), this->direction(), this->options, this->_quic_config->client_session_file());
   SSL_set_ex_data(tls->ssl_handle(), QUIC::ssl_quic_qc_index, static_cast<QUICConnection *>(this));
   TLSBasicSupport::bind(tls->ssl_handle(), this);
   TLSSessionResumptionSupport::bind(tls->ssl_handle(), this);
diff --git a/iocore/net/QUICPacketHandler.cc b/iocore/net/QUICPacketHandler.cc
index 4a69065..a30e9be 100644
--- a/iocore/net/QUICPacketHandler.cc
+++ b/iocore/net/QUICPacketHandler.cc
@@ -497,7 +497,7 @@ QUICPacketHandlerIn::_send_invalid_token_error(const uint8_t *initial_packet, ui
   QUICPacketFactory pf(ppki);
   QUICPacketHeaderProtector php(ppki);
   QUICCertConfig::scoped_config server_cert;
-  QUICTLS tls(ppki, server_cert->ssl_default.get(), NET_VCONNECTION_IN, {}, "", "");
+  QUICTLS tls(ppki, server_cert->ssl_default.get(), NET_VCONNECTION_IN, {}, "");
   tls.initialize_key_materials(dcid_in_initial, version_in_initial);
 
   // Create INITIAL packet
diff --git a/iocore/net/SSLClientCoordinator.cc b/iocore/net/SSLClientCoordinator.cc
index e27fd52..8c994b1 100644
--- a/iocore/net/SSLClientCoordinator.cc
+++ b/iocore/net/SSLClientCoordinator.cc
@@ -49,6 +49,7 @@ SSLClientCoordinator::startup()
   sslClientUpdate->attach("proxy.config.ssl.client.cert.filename");
   sslClientUpdate->attach("proxy.config.ssl.client.private_key.path");
   sslClientUpdate->attach("proxy.config.ssl.client.private_key.filename");
+  sslClientUpdate->attach("proxy.config.ssl.keylog_file");
   SSLConfig::startup();
   sslClientUpdate->attach("proxy.config.ssl.servername.filename");
   SNIConfig::startup();
diff --git a/iocore/net/SSLClientUtils.cc b/iocore/net/SSLClientUtils.cc
index 4d14569..36d057e 100644
--- a/iocore/net/SSLClientUtils.cc
+++ b/iocore/net/SSLClientUtils.cc
@@ -27,6 +27,7 @@
 
 #include "P_Net.h"
 #include "P_SSLClientUtils.h"
+#include "P_SSLConfig.h"
 #include "P_SSLNetVConnection.h"
 #include "YamlSNIConfig.h"
 #include "SSLDiags.h"
@@ -234,6 +235,12 @@ SSLInitClientContext(const SSLConfigParams *params)
     SSL_CTX_sess_set_new_cb(client_ctx, ssl_new_session_callback);
   }
 
+#if TS_HAS_TLS_KEYLOGGING
+  if (unlikely(TLSKeyLogger::is_enabled())) {
+    SSL_CTX_set_keylog_callback(client_ctx, TLSKeyLogger::ssl_keylog_cb);
+  }
+#endif
+
   return client_ctx;
 
 fail:
diff --git a/iocore/net/SSLConfig.cc b/iocore/net/SSLConfig.cc
index cecbb45..020c2bd 100644
--- a/iocore/net/SSLConfig.cc
+++ b/iocore/net/SSLConfig.cc
@@ -50,6 +50,7 @@
 #include "P_SSLSNI.h"
 #include "P_SSLCertLookup.h"
 #include "P_SSLSNI.h"
+#include "P_SSLUtils.h"
 #include "SSLDiags.h"
 #include "SSLSessionCache.h"
 #include "SSLSessionTicket.h"
@@ -114,6 +115,7 @@ SSLConfigParams::reset()
   client_tls13_cipher_suites                 = nullptr;
   server_groups_list                         = nullptr;
   client_groups_list                         = nullptr;
+  keylog_file                                = nullptr;
   client_ctx                                 = nullptr;
   clientCertLevel = client_verify_depth = verify_depth = 0;
   verifyServerPolicy                                   = YamlSNIConfig::Policy::DISABLED;
@@ -152,6 +154,7 @@ SSLConfigParams::cleanup()
   client_tls13_cipher_suites = static_cast<char *>(ats_free_null(client_tls13_cipher_suites));
   server_groups_list         = static_cast<char *>(ats_free_null(server_groups_list));
   client_groups_list         = static_cast<char *>(ats_free_null(client_groups_list));
+  keylog_file                = static_cast<char *>(ats_free_null(keylog_file));
 
   cleanupCTXTable();
   reset();
@@ -423,6 +426,13 @@ SSLConfigParams::initialize()
 
   REC_ReadConfigStringAlloc(client_groups_list, "proxy.config.ssl.client.groups_list");
 
+  REC_ReadConfigStringAlloc(keylog_file, "proxy.config.ssl.keylog_file");
+  if (keylog_file == nullptr) {
+    TLSKeyLogger::disable_keylogging();
+  } else {
+    TLSKeyLogger::enable_keylogging(keylog_file);
+  }
+
   REC_ReadConfigInt32(ssl_allow_client_renegotiation, "proxy.config.ssl.allow_client_renegotiation");
 
   REC_ReadConfigInt32(ssl_misc_max_iobuffer_size_index, "proxy.config.ssl.misc.io.max_buffer_index");
diff --git a/iocore/net/SSLUtils.cc b/iocore/net/SSLUtils.cc
index 43b4635..55d4628 100644
--- a/iocore/net/SSLUtils.cc
+++ b/iocore/net/SSLUtils.cc
@@ -46,7 +46,11 @@
 #include "SSLDiags.h"
 #include "SSLStats.h"
 
+#include <fcntl.h>
 #include <string>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/uio.h>
 #include <unistd.h>
 #include <termios.h>
 #include <vector>
@@ -100,6 +104,74 @@ static int ssl_vc_index = -1;
 static ink_mutex *mutex_buf      = nullptr;
 static bool open_ssl_initialized = false;
 
+// The caller of this function is responsible to acquire a unique_lock for
+// _mutex.
+void
+TLSKeyLogger::close_keylog_file()
+{
+  if (_fd == -1) {
+    return;
+  }
+  if (close(_fd) == -1) {
+    Error("Could not close keylog file: %s", strerror(errno));
+  }
+  _fd = -1;
+}
+
+void
+TLSKeyLogger::enable_keylogging_internal(const char *keylog_file)
+{
+#if TS_HAS_TLS_KEYLOGGING
+  Debug("ssl_keylog", "Enabling TLS key logging to: %s.", keylog_file);
+  std::unique_lock lock{_mutex};
+  if (keylog_file == nullptr) {
+    close_keylog_file();
+    Debug("ssl_keylog", "Received a nullptr for keylog_file: disabling keylogging.");
+    return;
+  }
+
+  _fd = open(keylog_file, O_WRONLY | O_APPEND | O_CREAT, S_IWUSR | S_IRUSR);
+  if (_fd == -1) {
+    Error("Could not open keylog file %s: %s", keylog_file, strerror(errno));
+    return;
+  }
+  Note("Opened %s for TLS key logging.", keylog_file);
+#else
+  Error("TLS keylogging is configured, but Traffic Server is not compiled with a version of OpenSSL that supports it.");
+  return;
+#endif /* TS_HAS_TLS_KEYLOGGING */
+}
+
+void
+TLSKeyLogger::disable_keylogging_internal()
+{
+  std::unique_lock lock{_mutex};
+  if (is_enabled()) {
+    Note("Disabling TLS key logging.");
+  }
+  close_keylog_file();
+  Debug("ssl_keylog", "TLS keylogging is disabled.");
+}
+
+void
+TLSKeyLogger::log(const char *line)
+{
+  std::shared_lock lock{_mutex};
+  if (!is_enabled()) {
+    return;
+  }
+
+  // writev() is guaranteed to be thread safe.
+  struct iovec vector[2];
+  vector[0].iov_base = const_cast<void *>(reinterpret_cast<const void *>(line));
+  vector[0].iov_len  = strlen(line);
+  vector[1].iov_base = const_cast<void *>(reinterpret_cast<const void *>("\n"));
+  vector[1].iov_len  = 1;
+  if (writev(_fd, vector, 2) <= 0) {
+    Error("Could not write TLS session key to key log file: %s", strerror(errno));
+  }
+}
+
 /* Using pthread thread ID and mutex functions directly, instead of
  * ATS this_ethread / ProxyMutex, so that other linked libraries
  * may use pthreads and openssl without confusing us here. (TS-2271).
@@ -1365,6 +1437,12 @@ SSLMultiCertConfigLoader::init_server_ssl_ctx(CertLoadData const &data, const SS
       goto fail;
     }
 
+#if TS_HAS_TLS_KEYLOGGING
+    if (unlikely(TLSKeyLogger::is_enabled()) && !this->_set_keylog_callback(ctx)) {
+      goto fail;
+    }
+#endif
+
     if (SSLConfigParams::init_ssl_ctx_cb) {
       SSLConfigParams::init_ssl_ctx_cb(ctx, true);
     }
@@ -1599,6 +1677,15 @@ SSLMultiCertConfigLoader::_set_alpn_callback(SSL_CTX *ctx)
   return true;
 }
 
+bool
+SSLMultiCertConfigLoader::_set_keylog_callback(SSL_CTX *ctx)
+{
+#if TS_HAS_TLS_KEYLOGGING
+  SSL_CTX_set_keylog_callback(ctx, TLSKeyLogger::ssl_keylog_cb);
+#endif
+  return true;
+}
+
 SSL_CTX *
 SSLCreateServerContext(const SSLConfigParams *params, const SSLMultiCertConfigParams *sslMultiCertSettings, const char *cert_path,
                        const char *key_path)
diff --git a/iocore/net/quic/QUICConfig.cc b/iocore/net/quic/QUICConfig.cc
index 171725d..50e75d7 100644
--- a/iocore/net/quic/QUICConfig.cc
+++ b/iocore/net/quic/QUICConfig.cc
@@ -28,6 +28,7 @@
 #include <records/I_RecHttp.h>
 
 #include "P_SSLConfig.h"
+#include "P_SSLUtils.h"
 
 #include "QUICGlobals.h"
 #include "QUICTransportParameters.h"
@@ -82,8 +83,8 @@ quic_init_client_ssl_ctx(const QUICConfigParams *params)
     SSL_CTX_sess_set_new_cb(ssl_ctx.get(), QUIC::ssl_client_new_session);
   }
 
-  if (params->client_keylog_file() != nullptr) {
-    SSL_CTX_set_keylog_callback(ssl_ctx.get(), QUIC::ssl_client_keylog_cb);
+  if (unlikely(TLSKeyLogger::is_enabled())) {
+    SSL_CTX_set_keylog_callback(ssl_ctx.get(), TLSKeyLogger::ssl_keylog_cb);
   }
 
   return ssl_ctx;
@@ -116,7 +117,6 @@ QUICConfigParams::initialize()
   REC_ReadConfigStringAlloc(this->_server_supported_groups, "proxy.config.quic.server.supported_groups");
   REC_ReadConfigStringAlloc(this->_client_supported_groups, "proxy.config.quic.client.supported_groups");
   REC_ReadConfigStringAlloc(this->_client_session_file, "proxy.config.quic.client.session_file");
-  REC_ReadConfigStringAlloc(this->_client_keylog_file, "proxy.config.quic.client.keylog_file");
   REC_ReadConfigStringAlloc(this->_qlog_dir, "proxy.config.quic.qlog_dir");
 
   // Transport Parameters
@@ -442,12 +442,6 @@ QUICConfigParams::client_session_file() const
 }
 
 const char *
-QUICConfigParams::client_keylog_file() const
-{
-  return this->_client_keylog_file;
-}
-
-const char *
 QUICConfigParams::qlog_dir() const
 {
   return this->_qlog_dir;
diff --git a/iocore/net/quic/QUICConfig.h b/iocore/net/quic/QUICConfig.h
index 0ce045a..08090e5 100644
--- a/iocore/net/quic/QUICConfig.h
+++ b/iocore/net/quic/QUICConfig.h
@@ -46,7 +46,6 @@ public:
   const char *server_supported_groups() const;
   const char *client_supported_groups() const;
   const char *client_session_file() const;
-  const char *client_keylog_file() const;
   const char *qlog_dir() const;
 
   shared_SSL_CTX client_ssl_ctx() const;
@@ -107,7 +106,6 @@ private:
   char *_server_supported_groups = nullptr;
   char *_client_supported_groups = nullptr;
   char *_client_session_file     = nullptr;
-  char *_client_keylog_file      = nullptr;
   char *_qlog_dir                = nullptr;
 
   shared_SSL_CTX _client_ssl_ctx = nullptr;
diff --git a/iocore/net/quic/QUICGlobals.cc b/iocore/net/quic/QUICGlobals.cc
index 9cef369..6dcd8f0 100644
--- a/iocore/net/quic/QUICGlobals.cc
+++ b/iocore/net/quic/QUICGlobals.cc
@@ -68,23 +68,6 @@ QUIC::ssl_client_new_session(SSL *ssl, SSL_SESSION *session)
 }
 
 void
-QUIC::ssl_client_keylog_cb(const SSL *ssl, const char *line)
-{
-  QUICTLS *qtls           = static_cast<QUICTLS *>(SSL_get_ex_data(ssl, QUIC::ssl_quic_tls_index));
-  const char *keylog_file = qtls->keylog_file();
-  std::ofstream file(keylog_file, std::ios_base::app);
-
-  if (!file.is_open()) {
-    QUICGlobalDebug("could not open keylog file: %s", keylog_file);
-    return;
-  }
-
-  file.write(line, strlen(line));
-  file.put('\n');
-  file.flush();
-}
-
-void
 QUIC::_register_stats()
 {
   quic_rsb = RecAllocateRawStatBlock(static_cast<int>(QUICStats::count));
diff --git a/iocore/net/quic/QUICTLS.cc b/iocore/net/quic/QUICTLS.cc
index a618e3a..d12fe5b 100644
--- a/iocore/net/quic/QUICTLS.cc
+++ b/iocore/net/quic/QUICTLS.cc
@@ -111,12 +111,6 @@ QUICTLS::session_file() const
   return this->_session_file;
 }
 
-const char *
-QUICTLS::keylog_file() const
-{
-  return this->_keylog_file;
-}
-
 QUICTLS::~QUICTLS()
 {
   SSL_free(this->_ssl);
diff --git a/iocore/net/quic/QUICTLS.h b/iocore/net/quic/QUICTLS.h
index 938e1e2..9a488c4 100644
--- a/iocore/net/quic/QUICTLS.h
+++ b/iocore/net/quic/QUICTLS.h
@@ -43,7 +43,7 @@ class QUICTLS : public QUICHandshakeProtocol
 {
 public:
   QUICTLS(QUICPacketProtectionKeyInfo &pp_key_info, SSL_CTX *ssl_ctx, NetVConnectionContext_t nvc_ctx,
-          const NetVCOptions &netvc_options, const char *session_file = nullptr, const char *keylog_file = nullptr);
+          const NetVCOptions &netvc_options, const char *session_file = nullptr);
   ~QUICTLS();
 
   // TODO: integrate with _early_data_processed
@@ -61,7 +61,6 @@ public:
   void set_remote_transport_parameters(std::shared_ptr<const QUICTransportParameters> tp) override;
 
   const char *session_file() const;
-  const char *keylog_file() const;
 
   // FIXME Should not exist
   SSL *ssl_handle();
@@ -110,7 +109,6 @@ private:
 
   static void _msg_cb(int write_p, int version, int content_type, const void *buf, size_t len, SSL *ssl, void *arg);
   const char *_session_file              = nullptr;
-  const char *_keylog_file               = nullptr;
   SSL *_ssl                              = nullptr;
   NetVConnectionContext_t _netvc_context = NET_VCONNECTION_UNSET;
   bool _early_data_processed             = false;
diff --git a/iocore/net/quic/QUICTLS_boringssl.cc b/iocore/net/quic/QUICTLS_boringssl.cc
index 01aabfa..091b186 100644
--- a/iocore/net/quic/QUICTLS_boringssl.cc
+++ b/iocore/net/quic/QUICTLS_boringssl.cc
@@ -173,12 +173,8 @@ QUICTLS::_msg_cb(int write_p, int version, int content_type, const void *buf, si
 }
 
 QUICTLS::QUICTLS(QUICPacketProtectionKeyInfo &pp_key_info, SSL_CTX *ssl_ctx, NetVConnectionContext_t nvc_ctx,
-                 const NetVCOptions &netvc_options, const char *session_file, const char *keylog_file)
-  : QUICHandshakeProtocol(pp_key_info),
-    _session_file(session_file),
-    _keylog_file(keylog_file),
-    _ssl(SSL_new(ssl_ctx)),
-    _netvc_context(nvc_ctx)
+                 const NetVCOptions &netvc_options, const char *session_file)
+  : QUICHandshakeProtocol(pp_key_info), _session_file(session_file), _ssl(SSL_new(ssl_ctx)), _netvc_context(nvc_ctx)
 {
   ink_assert(this->_netvc_context != NET_VCONNECTION_UNSET);
 
diff --git a/iocore/net/quic/QUICTLS_openssl.cc b/iocore/net/quic/QUICTLS_openssl.cc
index c34598d..5a5d3a9 100644
--- a/iocore/net/quic/QUICTLS_openssl.cc
+++ b/iocore/net/quic/QUICTLS_openssl.cc
@@ -125,12 +125,8 @@ QUICTLS::_msg_cb(int write_p, int version, int content_type, const void *buf, si
 }
 
 QUICTLS::QUICTLS(QUICPacketProtectionKeyInfo &pp_key_info, SSL_CTX *ssl_ctx, NetVConnectionContext_t nvc_ctx,
-                 const NetVCOptions &netvc_options, const char *session_file, const char *keylog_file)
-  : QUICHandshakeProtocol(pp_key_info),
-    _session_file(session_file),
-    _keylog_file(keylog_file),
-    _ssl(SSL_new(ssl_ctx)),
-    _netvc_context(nvc_ctx)
+                 const NetVCOptions &netvc_options, const char *session_file)
+  : QUICHandshakeProtocol(pp_key_info), _session_file(session_file), _ssl(SSL_new(ssl_ctx)), _netvc_context(nvc_ctx)
 {
   ink_assert(this->_netvc_context != NET_VCONNECTION_UNSET);
 
diff --git a/lib/perl/lib/Apache/TS/AdminClient.pm b/lib/perl/lib/Apache/TS/AdminClient.pm
index 258c592..b097c00 100644
--- a/lib/perl/lib/Apache/TS/AdminClient.pm
+++ b/lib/perl/lib/Apache/TS/AdminClient.pm
@@ -590,6 +590,7 @@ The Apache Traffic Server Administration Manual will explain what these strings
  proxy.config.ssl.TLSv1_3
  proxy.config.ssl.server.multicert.filename
  proxy.config.ssl.server.private_key.path
+ proxy.config.ssl.keylog_file
  proxy.config.stat_collector.interval
  proxy.config.stat_collector.port
  proxy.config.syslog_facility
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index fd00ca0..b7d6c59 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -1202,6 +1202,8 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.ssl.server.allow_early_data_params", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.ssl.keylog_file", RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
   //##############################################################################
   //#
   //# OCSP (Online Certificate Status Protocol) Stapling Configuration
@@ -1421,8 +1423,6 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.quic.client.session_file", RECD_STRING, nullptr , RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
-  {RECT_CONFIG, "proxy.config.quic.client.keylog_file", RECD_STRING, nullptr , RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
-  ,
   {RECT_CONFIG, "proxy.config.quic.qlog_dir", RECD_STRING, nullptr , RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
   // Transport Parameters
diff --git a/tests/gold_tests/tls/tls_session_key_logging.replay.yaml b/tests/gold_tests/tls/tls_session_key_logging.replay.yaml
new file mode 100644
index 0000000..9d37fac
--- /dev/null
+++ b/tests/gold_tests/tls/tls_session_key_logging.replay.yaml
@@ -0,0 +1,48 @@
+#  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.
+
+meta:
+  version: "1.0"
+
+sessions:
+- protocol:
+  - name: http
+    version: 1
+  - name: tls
+  - name: tcp
+  - name: ip
+
+  transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /for/tls
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ Connection, keep-alive ]
+        - [ Content-Length, 16 ]
+        - [ uuid, 2 ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 32 ]
+
+    proxy-response:
+      status: 200
diff --git a/tests/gold_tests/tls/tls_session_key_logging.test.py b/tests/gold_tests/tls/tls_session_key_logging.test.py
new file mode 100644
index 0000000..1e40363
--- /dev/null
+++ b/tests/gold_tests/tls/tls_session_key_logging.test.py
@@ -0,0 +1,96 @@
+'''
+Test TLS secrets logging.
+'''
+#  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
+
+
+Test.Summary = '''
+Test TLS secrets logging.
+'''
+
+
+class TlsKeyloggingTest:
+
+    replay_file = "tls_session_key_logging.replay.yaml"
+    keylog_file = os.path.join(Test.RunDirectory, "tls_secrets.txt")
+
+    server_counter = 0
+    ts_counter = 0
+    client_counter = 0
+
+    def __init__(self, enable_secrets_logging):
+        self.setupOriginServer()
+        self.setupTS(enable_secrets_logging)
+
+    def setupOriginServer(self):
+        server_name = f"server_{TlsKeyloggingTest.server_counter}"
+        TlsKeyloggingTest.server_counter += 1
+        self.server = Test.MakeVerifierServerProcess(
+            server_name, TlsKeyloggingTest.replay_file)
+
+    def setupTS(self, enable_secrets_logging):
+        ts_name = f"ts_{TlsKeyloggingTest.ts_counter}"
+        TlsKeyloggingTest.ts_counter += 1
+        self.ts = Test.MakeATSProcess(ts_name, enable_tls=True, enable_cache=False)
+        self.ts.addDefaultSSLFiles()
+        self.ts.Disk.records_config.update({
+            "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}',
+            "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}',
+            "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE',
+
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': 'ssl_keylog'
+        })
+        self.ts.Disk.ssl_multicert_config.AddLine(
+            'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+        )
+        self.ts.Disk.remap_config.AddLine(
+            f'map / https://127.0.0.1:{self.server.Variables.https_port}'
+        )
+        if enable_secrets_logging:
+            self.ts.Disk.records_config.update({
+                'proxy.config.ssl.keylog_file': TlsKeyloggingTest.keylog_file,
+            })
+
+            self.ts.Disk.diags_log.Content += Testers.ContainsExpression(
+                f"Opened {TlsKeyloggingTest.keylog_file} for TLS key logging",
+                "Verify the user was notified of TLS secrets logging.")
+            self.ts.Disk.File(TlsKeyloggingTest.keylog_file, id="keylog", exists=True)
+            # It would be nice to verify the content of certain lines in the
+            # keylog file, but the content is dependent upon the particular TLS
+            # protocol version. Thus I'm hesitant to add ContainsExpression
+            # checks here which will be fragile and eventually become outdated.
+        else:
+            self.ts.Disk.File(TlsKeyloggingTest.keylog_file, exists=False)
+
+    def run(self):
+        tr = Test.AddTestRun()
+        tr.Processes.Default.StartBefore(self.server)
+        tr.Processes.Default.StartBefore(self.ts)
+
+        client_name = f"client_{TlsKeyloggingTest.client_counter}"
+        TlsKeyloggingTest.client_counter += 1
+        tr.AddVerifierClientProcess(
+            client_name,
+            TlsKeyloggingTest.replay_file,
+            https_ports=[self.ts.Variables.ssl_port])
+
+
+TlsKeyloggingTest(enable_secrets_logging=False).run()
+TlsKeyloggingTest(enable_secrets_logging=True).run()