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

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


The following commit(s) were added to refs/heads/master by this push:
     new 53034a3a14 [KYUUBI #6866] Add metrics for SSL keystore expiration time
53034a3a14 is described below

commit 53034a3a14db68491be5e55e0462a1059f2f3a59
Author: Wang, Fei <[email protected]>
AuthorDate: Thu Dec 26 14:04:05 2024 +0800

    [KYUUBI #6866] Add metrics for SSL keystore expiration time
    
    ### Why are the changes needed?
    
    Add metrics for SSL keystore expiration, then we can add alert if the 
keystore will expire in 1 month.
    
    ### How was this patch tested?
    
    Integration testing.
    <img width="1721" alt="image" 
src="https://github.com/user-attachments/assets/f4ef6af6-923b-403c-a80d-06dbb80dbe1c";
 />
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No.
    
    Closes #6866 from turboFei/keystore_expire.
    
    Closes #6866
    
    77c6db0a7 [Wang, Fei] Add metrics for SSL keystore expiration time #6866
    
    Authored-by: Wang, Fei <[email protected]>
    Signed-off-by: Cheng Pan <[email protected]>
---
 .../kyuubi/service/TBinaryFrontendService.scala    | 10 +++-
 .../apache/kyuubi/metrics/MetricsConstants.scala   |  2 +
 .../server/KyuubiTBinaryFrontendService.scala      |  6 ++
 .../kyuubi/server/KyuubiTHttpFrontendService.scala | 17 ++++--
 .../scala/org/apache/kyuubi/util/SSLUtils.scala    | 70 ++++++++++++++++++++++
 5 files changed, 98 insertions(+), 7 deletions(-)

diff --git 
a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
 
b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
index 92b3a8a810..43060946ff 100644
--- 
a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
+++ 
b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
@@ -53,6 +53,10 @@ abstract class TBinaryFrontendService(name: String)
   private var _actualPort: Int = _
   override protected lazy val actualPort: Int = _actualPort
 
+  protected var keyStorePath: Option[String] = None
+  protected var keyStorePassword: Option[String] = None
+  protected var keyStoreType: Option[String] = None
+
   // Removed OOM hook since Kyuubi #1800 to respect the hive server2 #2383
 
   override def initialize(conf: KyuubiConf): Unit = synchronized {
@@ -73,9 +77,9 @@ abstract class TBinaryFrontendService(name: String)
       val tServerSocket =
         // only enable ssl for server side
         if (isServer() && conf.get(FRONTEND_THRIFT_BINARY_SSL_ENABLED)) {
-          val keyStorePath = conf.get(FRONTEND_SSL_KEYSTORE_PATH)
-          val keyStorePassword = conf.get(FRONTEND_SSL_KEYSTORE_PASSWORD)
-          val keyStoreType = conf.get(FRONTEND_SSL_KEYSTORE_TYPE)
+          keyStorePath = conf.get(FRONTEND_SSL_KEYSTORE_PATH)
+          keyStorePassword = conf.get(FRONTEND_SSL_KEYSTORE_PASSWORD)
+          keyStoreType = conf.get(FRONTEND_SSL_KEYSTORE_TYPE)
           val keyStoreAlgorithm = conf.get(FRONTEND_SSL_KEYSTORE_ALGORITHM)
           val disallowedSslProtocols = 
conf.get(FRONTEND_THRIFT_BINARY_SSL_DISALLOWED_PROTOCOLS)
           val includeCipherSuites = 
conf.get(FRONTEND_THRIFT_BINARY_SSL_INCLUDE_CIPHER_SUITES)
diff --git 
a/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
 
b/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
index 336060e8f7..40f8b7c394 100644
--- 
a/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
+++ 
b/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
@@ -37,6 +37,8 @@ object MetricsConstants {
   final private val THRIFT_BINARY_CONN = KYUUBI + "thrift.binary.connection."
   final private val REST_CONN = KYUUBI + "rest.connection."
 
+  final val THRIFT_SSL_CERT_EXPIRATION = KYUUBI + "thrift.ssl.cert.expiration"
+
   final val CONN_OPEN: String = CONN + "opened"
   final val CONN_FAIL: String = CONN + "failed"
   final val CONN_TOTAL: String = CONN + "total"
diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala
 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala
index 278aa67fed..2d06bed016 100644
--- 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala
+++ 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala
@@ -35,6 +35,7 @@ import org.apache.kyuubi.session.KyuubiSessionImpl
 import org.apache.kyuubi.shaded.hive.service.rpc.thrift._
 import org.apache.kyuubi.shaded.thrift.protocol.TProtocol
 import org.apache.kyuubi.shaded.thrift.server.ServerContext
+import org.apache.kyuubi.util.SSLUtils
 
 final class KyuubiTBinaryFrontendService(
     override val serverable: Serverable)
@@ -122,4 +123,9 @@ final class KyuubiTBinaryFrontendService(
     resp.setStatus(notSupportTokenErrorStatus)
     resp
   }
+
+  override def start(): Unit = {
+    super.start()
+    SSLUtils.tracingThriftSSLCertExpiration(keyStorePath, keyStorePassword, 
keyStoreType)
+  }
 }
diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala
 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala
index b99f50310f..c19743e4a2 100644
--- 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala
+++ 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala
@@ -50,7 +50,7 @@ import 
org.apache.kyuubi.service.TFrontendService.{CURRENT_SERVER_CONTEXT, OK_ST
 import org.apache.kyuubi.session.KyuubiSessionImpl
 import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TCLIService, 
TOpenSessionReq, TOpenSessionResp}
 import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol
-import org.apache.kyuubi.util.NamedThreadFactory
+import org.apache.kyuubi.util.{NamedThreadFactory, SSLUtils}
 
 /**
  * Apache Thrift based hive service rpc
@@ -67,6 +67,10 @@ final class KyuubiTHttpFrontendService(
   override protected lazy val actualPort: Int = portNum
   override protected lazy val serverSocket: ServerSocket = null
 
+  private var keyStorePath: Option[String] = None
+  private var keyStorePassword: Option[String] = None
+  private var keyStoreType: Option[String] = None
+
   private var server: Option[Server] = None
   private val APPLICATION_THRIFT = "application/x-thrift"
 
@@ -122,7 +126,7 @@ final class KyuubiTHttpFrontendService(
       // Change connector if SSL is used
       val connector =
         if (useSsl) {
-          val keyStorePath = conf.get(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH)
+          keyStorePath = conf.get(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH)
 
           if (keyStorePath.isEmpty) {
             throw new 
IllegalArgumentException(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH.key +
@@ -130,7 +134,7 @@ final class KyuubiTHttpFrontendService(
               FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH.doc)
           }
 
-          val keyStorePassword = 
conf.get(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD)
+          keyStorePassword = 
conf.get(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD)
           if (keyStorePassword.isEmpty) {
             throw new 
IllegalArgumentException(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD.key +
               " Not configured for SSL connection. please set the key with: " +
@@ -140,7 +144,7 @@ final class KyuubiTHttpFrontendService(
           val sslContextFactory = new SslContextFactory.Server
           val excludedProtocols = 
conf.get(FRONTEND_THRIFT_HTTP_SSL_PROTOCOL_BLACKLIST)
           val excludeCipherSuites = 
conf.get(FRONTEND_THRIFT_HTTP_SSL_EXCLUDE_CIPHER_SUITES)
-          val keyStoreType = conf.get(FRONTEND_SSL_KEYSTORE_TYPE)
+          keyStoreType = conf.get(FRONTEND_SSL_KEYSTORE_TYPE)
           val keyStoreAlgorithm = conf.get(FRONTEND_SSL_KEYSTORE_ALGORITHM)
           info("Thrift HTTP Server SSL: adding excluded protocols: " +
             String.join(",", excludedProtocols: _*))
@@ -359,4 +363,9 @@ final class KyuubiTHttpFrontendService(
 
     ret
   }
+
+  override def start(): Unit = {
+    super.start()
+    SSLUtils.tracingThriftSSLCertExpiration(keyStorePath, keyStorePassword, 
keyStoreType)
+  }
 }
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/util/SSLUtils.scala 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/util/SSLUtils.scala
new file mode 100644
index 0000000000..f73f87b904
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/util/SSLUtils.scala
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+package org.apache.kyuubi.util
+import java.io.FileInputStream
+import java.security.KeyStore
+import java.security.cert.X509Certificate
+
+import scala.collection.JavaConverters._
+
+import org.apache.kyuubi.{Logging, Utils}
+import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem}
+
+object SSLUtils extends Logging {
+
+  /**
+   * Get the keystore certificate latest expiration time.
+   */
+  private def getKeyStoreExpirationTime(
+      keyStorePath: String,
+      keyStorePassword: String,
+      keyStoreType: Option[String]): Option[Long] = {
+    try {
+      val keyStore = 
KeyStore.getInstance(keyStoreType.getOrElse(KeyStore.getDefaultType))
+      keyStore.load(new FileInputStream(keyStorePath), 
keyStorePassword.toCharArray)
+      keyStore.aliases().asScala.toSeq.map { alias =>
+        
keyStore.getCertificate(alias).asInstanceOf[X509Certificate].getNotAfter.getTime
+      }.sorted.headOption
+    } catch {
+      case e: Throwable =>
+        error("Error getting keystore expiration time.", e)
+        None
+    }
+  }
+
+  def tracingThriftSSLCertExpiration(
+      keyStorePath: Option[String],
+      keyStorePassword: Option[String],
+      keyStoreType: Option[String]): Unit = {
+    if (keyStorePath.isDefined && keyStorePassword.isDefined) {
+      SSLUtils.getKeyStoreExpirationTime(
+        keyStorePath.get,
+        keyStorePassword.get,
+        keyStoreType).foreach { expiration =>
+        info(s"Thrift SSL Serve KeyStore ${keyStorePath.get} will expire at:" +
+          s" ${Utils.getDateFromTimestamp(expiration)}")
+        MetricsSystem.tracing { ms =>
+          ms.registerGauge(
+            MetricsConstants.THRIFT_SSL_CERT_EXPIRATION,
+            expiration - System.currentTimeMillis(),
+            0L)
+        }
+      }
+    }
+  }
+}

Reply via email to