diff --git a/docs/deployment/settings.md b/docs/deployment/settings.md
index 31533e681..89aff1849 100644
--- a/docs/deployment/settings.md
+++ b/docs/deployment/settings.md
@@ -350,6 +350,7 @@ Key | Default | Meaning | Type | Since
kyuubi.server.limit.connections.per.user|
<undefined>
|Maximum kyuubi server connections per user. Any user exceeding this limit will not be allowed to connect.
|int
|1.6.0
kyuubi.server.limit.connections.per.user.ipaddress|<undefined>
|Maximum kyuubi server connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect.
|int
|1.6.0
kyuubi.server.name|<undefined>
|The name of Kyuubi Server.
|string
|1.5.0
+kyuubi.server.redaction.regex|<undefined>
|Regex to decide which Kyuubi contain sensitive information. When this regex matches a property key or value, the value is redacted from the various logs.
||1.6.0
### Session
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala
index e97cece85..1789d4fe9 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala
@@ -25,6 +25,7 @@ import java.util.{Properties, TimeZone, UUID}
import scala.collection.JavaConverters._
import scala.util.control.NonFatal
+import scala.util.matching.Regex
import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.time.DateFormatUtils
@@ -288,4 +289,53 @@ object Utils extends Logging {
}
}
}
+
+ val REDACTION_REPLACEMENT_TEXT = "*********(redacted)"
+
+ private val PATTERN_FOR_KEY_VALUE_ARG = "(.+?)=(.+)".r
+
+ def redactCommandLineArgs(conf: KyuubiConf, commands: Array[String]): Array[String] = {
+ val redactionPattern = conf.get(SERVER_SECRET_REDACTION_PATTERN)
+ var nextKV = false
+ commands.map {
+ case PATTERN_FOR_KEY_VALUE_ARG(key, value) if nextKV =>
+ val (_, newValue) = redact(redactionPattern, Seq((key, value))).head
+ nextKV = false
+ s"$key=$newValue"
+
+ case cmd if cmd == "--conf" =>
+ nextKV = true
+ cmd
+
+ case cmd =>
+ cmd
+ }
+ }
+
+ /**
+ * Redact the sensitive values in the given map. If a map key matches the redaction pattern then
+ * its value is replaced with a dummy text.
+ */
+ def redact[K, V](regex: Option[Regex], kvs: Seq[(K, V)]): Seq[(K, V)] = {
+ regex match {
+ case None => kvs
+ case Some(r) => redact(r, kvs)
+ }
+ }
+
+ private def redact[K, V](redactionPattern: Regex, kvs: Seq[(K, V)]): Seq[(K, V)] = {
+ kvs.map {
+ case (key: String, value: String) =>
+ redactionPattern.findFirstIn(key)
+ .orElse(redactionPattern.findFirstIn(value))
+ .map { _ => (key, REDACTION_REPLACEMENT_TEXT) }
+ .getOrElse((key, value))
+ case (key, value: String) =>
+ redactionPattern.findFirstIn(value)
+ .map { _ => (key, REDACTION_REPLACEMENT_TEXT) }
+ .getOrElse((key, value))
+ case (key, value) =>
+ (key, value)
+ }.asInstanceOf[Seq[(K, V)]]
+ }
}
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/ConfigBuilder.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/ConfigBuilder.scala
index 8863aabc3..d834de44a 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/ConfigBuilder.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/ConfigBuilder.scala
@@ -18,8 +18,10 @@
package org.apache.kyuubi.config
import java.time.Duration
+import java.util.regex.PatternSyntaxException
import scala.util.{Failure, Success, Try}
+import scala.util.matching.Regex
private[kyuubi] case class ConfigBuilder(key: String) {
@@ -119,6 +121,18 @@ private[kyuubi] case class ConfigBuilder(key: String) {
_onCreate.foreach(_(entry))
entry
}
+
+ def regexConf: TypedConfigBuilder[Regex] = {
+ def regexFromString(str: String, key: String): Regex = {
+ try str.r
+ catch {
+ case e: PatternSyntaxException =>
+ throw new IllegalArgumentException(s"$key should be a regex, but was $str", e)
+ }
+ }
+
+ new TypedConfigBuilder(this, regexFromString(_, this.key), _.toString)
+ }
}
private[kyuubi] case class TypedConfigBuilder[T](
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
index 49c6a649f..1aea159db 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
@@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import scala.collection.JavaConverters._
+import scala.util.matching.Regex
import org.apache.kyuubi.{Logging, Utils}
import org.apache.kyuubi.config.KyuubiConf._
@@ -1490,4 +1491,12 @@ object KyuubiConf {
.version("1.6.0")
.booleanConf
.createWithDefault(false)
+
+ val SERVER_SECRET_REDACTION_PATTERN: OptionalConfigEntry[Regex] =
+ buildConf("kyuubi.server.redaction.regex")
+ .doc("Regex to decide which Kyuubi contain sensitive information. When this regex matches " +
+ "a property key or value, the value is redacted from the various logs.")
+ .version("1.6.0")
+ .regexConf
+ .createOptional
}
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala
index 3b50b3066..4c667447c 100644
--- a/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala
@@ -23,9 +23,12 @@ import java.nio.file.{Files, Paths}
import java.security.PrivilegedExceptionAction
import java.util.Properties
+import scala.collection.mutable.ArrayBuffer
+
import org.apache.hadoop.security.UserGroupInformation
import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf.SERVER_SECRET_REDACTION_PATTERN
class UtilsSuite extends KyuubiFunSuite {
@@ -160,4 +163,51 @@ class UtilsSuite extends KyuubiFunSuite {
val exception2 = intercept[IllegalArgumentException](Utils.fromCommandLineArgs(args2, conf))
assert(exception2.getMessage.contains("Illegal argument: a"))
}
+
+ test("redact sensitive information in command line args") {
+ val conf = new KyuubiConf()
+ conf.set(SERVER_SECRET_REDACTION_PATTERN, "(?i)secret|password".r)
+
+ val buffer = new ArrayBuffer[String]()
+ buffer += "main"
+ buffer += "--conf"
+ buffer += "kyuubi.my.password=sensitive_value"
+ buffer += "--conf"
+ buffer += "kyuubi.regular.property1=regular_value"
+ buffer += "--conf"
+ buffer += "kyuubi.my.secret=sensitive_value"
+ buffer += "--conf"
+ buffer += "kyuubi.regular.property2=regular_value"
+
+ val commands = buffer.toArray
+
+ // Redact sensitive information
+ val redactedCmdArgs = Utils.redactCommandLineArgs(conf, commands)
+
+ val expectBuffer = new ArrayBuffer[String]()
+ expectBuffer += "main"
+ expectBuffer += "--conf"
+ expectBuffer += "kyuubi.my.password=" + Utils.REDACTION_REPLACEMENT_TEXT
+ expectBuffer += "--conf"
+ expectBuffer += "kyuubi.regular.property1=regular_value"
+ expectBuffer += "--conf"
+ expectBuffer += "kyuubi.my.secret=" + Utils.REDACTION_REPLACEMENT_TEXT
+ expectBuffer += "--conf"
+ expectBuffer += "kyuubi.regular.property2=regular_value"
+
+ assert(expectBuffer.toArray === redactedCmdArgs)
+ }
+
+ test("redact sensitive information") {
+ val secretKeys = Some("my.password".r)
+ assert(Utils.redact(secretKeys, Seq(("kyuubi.my.password", "12345"))) ===
+ Seq(("kyuubi.my.password", Utils.REDACTION_REPLACEMENT_TEXT)))
+ assert(Utils.redact(secretKeys, Seq(("anything", "kyuubi.my.password=12345"))) ===
+ Seq(("anything", Utils.REDACTION_REPLACEMENT_TEXT)))
+ assert(Utils.redact(secretKeys, Seq((999, "kyuubi.my.password=12345"))) ===
+ Seq((999, Utils.REDACTION_REPLACEMENT_TEXT)))
+ // Do not redact when value type is not string
+ assert(Utils.redact(secretKeys, Seq(("my.password", 12345))) ===
+ Seq(("my.password", 12345)))
+ }
}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala
index 5db3b63d7..14235fc53 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala
@@ -185,7 +185,8 @@ private[kyuubi] class EngineRef(
MetricsSystem.tracing(_.incCount(ENGINE_TOTAL))
try {
- info(s"Launching engine:\n$builder")
+ val redactedCmd = builder.toString
+ info(s"Launching engine:\n$redactedCmd")
val process = builder.start
var exitValue: Option[Int] = None
while (engineRef.isEmpty) {
@@ -205,7 +206,7 @@ private[kyuubi] class EngineRef(
process.destroyForcibly()
MetricsSystem.tracing(_.incCount(MetricRegistry.name(ENGINE_TIMEOUT, appUser)))
throw KyuubiSQLException(
- s"Timeout($timeout ms) to launched $engineType engine with $builder. $killMessage",
+ s"Timeout($timeout ms) to launched $engineType engine with $redactedCmd. $killMessage",
builder.getError)
}
engineRef = discoveryClient.getEngineByRefId(engineSpace, engineRefId)
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala
index 53dd7fe0f..d4f6fd618 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala
@@ -270,7 +270,7 @@ trait ProcBuilder {
if (commands == null) {
super.toString()
} else {
- commands.map {
+ Utils.redactCommandLineArgs(conf, commands).map {
case arg if arg.startsWith("--") => s"\\\n\t$arg"
case arg => arg
}.mkString(" ")
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala
index 8c490174e..34d077646 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala
@@ -105,7 +105,7 @@ class HiveProcessBuilder(
buffer.toArray
}
- override def toString: String = commands.mkString("\n")
+ override def toString: String = Utils.redactCommandLineArgs(conf, commands).mkString("\n")
override def shortName: String = "hive"
}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala
index 0dc9b09d5..7b9e81da6 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala
@@ -55,7 +55,7 @@ class SparkProcessBuilder(
var allConf = conf.getAll
- // if enable sasl kerberos authentication for zookeeper, need to upload the server ketab file
+ // if enable sasl kerberos authentication for zookeeper, need to upload the server keytab file
if (AuthTypes.withName(conf.get(HighAvailabilityConf.HA_ZK_ENGINE_AUTH_TYPE))
== AuthTypes.KERBEROS) {
allConf = allConf ++ zkAuthKeytabFileConf(allConf)
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala
index de3cbc5ca..191e24ca2 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala
@@ -98,5 +98,5 @@ class TrinoProcessBuilder(
override def shortName: String = "trino"
- override def toString: String = commands.mkString("\n")
+ override def toString: String = Utils.redactCommandLineArgs(conf, commands).mkString("\n")
}