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

HTHou pushed a commit to branch codex/thrift-client-mtls
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit e6499595cfc662f602effd54bd4af2384bf74efb
Author: HTHou <[email protected]>
AuthorDate: Thu Jun 25 10:41:51 2026 +0800

    Support Thrift client mutual TLS
---
 .../it/env/cluster/config/MppCommonConfig.java     |   6 +
 .../env/cluster/config/MppSharedCommonConfig.java  |   7 +
 .../iotdb/it/env/cluster/env/AbstractEnv.java      |  29 +++
 .../it/env/remote/config/RemoteCommonConfig.java   |   5 +
 .../org/apache/iotdb/itbase/env/CommonConfig.java  |   2 +
 .../iotdb/session/it/IoTDBClientMutualSSLIT.java   | 284 +++++++++++++++++++++
 .../java/org/apache/iotdb/cli/AbstractCli.java     |  44 ++++
 .../src/main/java/org/apache/iotdb/cli/Cli.java    |  23 +-
 .../org/apache/iotdb/tool/common/Constants.java    |   8 +
 .../org/apache/iotdb/tool/common/OptionsUtil.java  |  20 ++
 .../apache/iotdb/tool/data/AbstractDataTool.java   |  21 ++
 .../iotdb/tool/schema/AbstractSchemaTool.java      |  18 ++
 .../main/java/org/apache/iotdb/jdbc/Config.java    |   4 +
 .../org/apache/iotdb/jdbc/IoTDBConnection.java     |   4 +-
 .../apache/iotdb/jdbc/IoTDBConnectionParams.java   |  18 ++
 .../src/main/java/org/apache/iotdb/jdbc/Utils.java |   6 +
 .../test/java/org/apache/iotdb/jdbc/UtilsTest.java |   8 +
 .../apache/iotdb/rpc/BaseRpcTransportFactory.java  |  16 +-
 .../iotdb/session/AbstractSessionBuilder.java      |   2 +
 .../org/apache/iotdb/session/NodesSupplier.java    |  12 +
 .../java/org/apache/iotdb/session/Session.java     |  16 ++
 .../apache/iotdb/session/SessionConnection.java    |  18 +-
 .../apache/iotdb/session/TableSessionBuilder.java  |  24 ++
 .../org/apache/iotdb/session/ThriftConnection.java |   6 +-
 .../org/apache/iotdb/session/pool/SessionPool.java |  22 ++
 .../session/pool/TableSessionPoolBuilder.java      |  24 ++
 .../iotdb/db/service/ExternalRPCService.java       |  80 ++++--
 .../conf/iotdb-system.properties.template          |   7 +
 .../apache/iotdb/commons/conf/CommonConfig.java    |  11 +
 .../iotdb/commons/conf/CommonDescriptor.java       |   4 +
 30 files changed, 707 insertions(+), 42 deletions(-)

diff --git 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
index 4dd7f857162..bd2fb84e0b6 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
@@ -632,6 +632,12 @@ public class MppCommonConfig extends MppBaseConfig 
implements CommonConfig {
     return this;
   }
 
+  @Override
+  public CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+    setProperty("thrift_ssl_client_auth", String.valueOf(thriftSSLClientAuth));
+    return this;
+  }
+
   @Override
   public CommonConfig setEnableInternalSSL(boolean enableInternalSSL) {
     setProperty("enable_internal_ssl", String.valueOf(enableInternalSSL));
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
index 8e0398de309..b1c2a4f8d6b 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
@@ -658,6 +658,13 @@ public class MppSharedCommonConfig implements CommonConfig 
{
     return this;
   }
 
+  @Override
+  public CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+    cnConfig.setThriftSSLClientAuth(thriftSSLClientAuth);
+    dnConfig.setThriftSSLClientAuth(thriftSSLClientAuth);
+    return this;
+  }
+
   @Override
   public CommonConfig setEnableInternalSSL(boolean enableInternalSSL) {
     cnConfig.setEnableInternalSSL(enableInternalSSL);
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
index 342b4aba26d..e5d4e19834d 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
@@ -692,6 +692,10 @@ public abstract class AbstractEnv implements BaseEnv {
     return 
Boolean.parseBoolean(getDataNodeCommonConfigProperty("enable_thrift_ssl", 
"false"));
   }
 
+  private boolean isThriftSSLClientAuthEnabled() {
+    return 
Boolean.parseBoolean(getDataNodeCommonConfigProperty("thrift_ssl_client_auth", 
"false"));
+  }
+
   private String getDataNodeCommonConfigProperty(final String key, final 
String defaultValue) {
     return ((MppCommonConfig) clusterConfig.getDataNodeCommonConfig())
         .getProperty(key, defaultValue);
@@ -711,6 +715,11 @@ public abstract class AbstractEnv implements BaseEnv {
       putIfPresent(
           info, Config.TRUST_STORE_PWD, 
getDataNodeCommonConfigProperty("trust_store_pwd", ""));
       putIfPresent(info, Config.SSL_PROTOCOL, getClientSSLProtocol());
+      if (isThriftSSLClientAuthEnabled()) {
+        putIfPresent(info, Config.KEY_STORE, 
getDataNodeCommonConfigProperty("key_store_path", ""));
+        putIfPresent(
+            info, Config.KEY_STORE_PWD, 
getDataNodeCommonConfigProperty("key_store_pwd", ""));
+      }
     }
     return info;
   }
@@ -728,6 +737,11 @@ public abstract class AbstractEnv implements BaseEnv {
           .trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
           .trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd", 
""))
           .sslProtocol(getClientSSLProtocol());
+      if (isThriftSSLClientAuthEnabled()) {
+        builder
+            .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+            .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+      }
     }
     return builder;
   }
@@ -739,6 +753,11 @@ public abstract class AbstractEnv implements BaseEnv {
           .trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
           .trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd", 
""))
           .sslProtocol(getClientSSLProtocol());
+      if (isThriftSSLClientAuthEnabled()) {
+        builder
+            .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+            .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+      }
     }
     return builder;
   }
@@ -750,6 +769,11 @@ public abstract class AbstractEnv implements BaseEnv {
           .trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
           .trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd", 
""))
           .sslProtocol(getClientSSLProtocol());
+      if (isThriftSSLClientAuthEnabled()) {
+        builder
+            .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+            .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+      }
     }
     return builder;
   }
@@ -761,6 +785,11 @@ public abstract class AbstractEnv implements BaseEnv {
           .trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
           .trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd", 
""))
           .sslProtocol(getClientSSLProtocol());
+      if (isThriftSSLClientAuthEnabled()) {
+        builder
+            .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+            .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+      }
     }
     return builder;
   }
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
 
b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
index a2d10c01009..ba1f7106dd6 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
@@ -447,6 +447,11 @@ public class RemoteCommonConfig implements CommonConfig {
     return this;
   }
 
+  @Override
+  public CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+    return this;
+  }
+
   @Override
   public CommonConfig setSubscriptionPrefetchTsFileBatchMaxDelayInMs(
       int subscriptionPrefetchTsFileBatchMaxDelayInMs) {
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java 
b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
index 5b51a6a8cf9..5a7a004fa88 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
@@ -203,6 +203,8 @@ public interface CommonConfig {
 
   CommonConfig setEnableThriftClientSSL(boolean enableThriftClientSSL);
 
+  CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth);
+
   CommonConfig setEnableInternalSSL(boolean enableInternalSSL);
 
   CommonConfig setKeyStorePath(String keyStorePath);
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBClientMutualSSLIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBClientMutualSSLIT.java
new file mode 100644
index 00000000000..0feb9ba6f30
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBClientMutualSSLIT.java
@@ -0,0 +1,284 @@
+/*
+ * 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.iotdb.session.it;
+
+import org.apache.iotdb.isession.ISession;
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.isession.pool.ISessionPool;
+import org.apache.iotdb.isession.pool.SessionDataSetWrapper;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+import org.apache.iotdb.jdbc.Config;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.session.Session;
+import org.apache.iotdb.session.TableSessionBuilder;
+import org.apache.iotdb.session.pool.SessionPool;
+
+import org.apache.tsfile.read.common.RowRecord;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({
+  LocalStandaloneIT.class,
+  ClusterIT.class,
+  TableLocalStandaloneIT.class,
+  TableClusterIT.class
+})
+public class IoTDBClientMutualSSLIT {
+
+  private static final String STORE_PASSWORD = "thrift";
+  private static String keyDir;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    keyDir =
+        System.getProperty("user.dir")
+            + File.separator
+            + "target"
+            + File.separator
+            + "test-classes"
+            + File.separator;
+
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setEnableThriftClientSSL(true)
+        .setThriftSSLClientAuth(true)
+        .setKeyStorePath(keyStorePath())
+        .setKeyStorePwd(STORE_PASSWORD)
+        .setTrustStorePath(trustStorePath())
+        .setTrustStorePwd(STORE_PASSWORD)
+        .setSslProtocol(SessionConfig.DEFAULT_SSL_PROTOCOL);
+    EnvFactory.getEnv().initClusterEnvironment();
+  }
+
+  @After
+  public void tearDown() {
+    try (ISession session = newMutualSSLSession()) {
+      deleteTreeDatabase(session, "root.client_mtls_tree");
+      deleteTreeDatabase(session, "root.client_mtls_pool");
+      deleteTreeDatabase(session, "root.client_mtls_jdbc");
+    } catch (Exception ignored) {
+      // ignored
+    }
+    try (ITableSession session = newMutualSSLTableSession()) {
+      session.executeNonQueryStatement("DROP DATABASE IF EXISTS 
client_mtls_table");
+    } catch (Exception ignored) {
+      // ignored
+    }
+  }
+
+  @AfterClass
+  public static void tearDownClass() {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void sslClientWithoutKeyStoreCanNotConnectWhenClientAuthRequired() {
+    final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+    final Session session =
+        new Session.Builder()
+            .host(dataNode.getIp())
+            .port(dataNode.getPort())
+            .useSSL(true)
+            .trustStore(trustStorePath())
+            .trustStorePwd(STORE_PASSWORD)
+            .build();
+
+    assertThrows(IoTDBConnectionException.class, session::open);
+  }
+
+  @Test
+  public void treeSessionCanConnectWithMutualSSL() throws Exception {
+    try (ISession session = newMutualSSLSession()) {
+      session.executeNonQueryStatement("CREATE DATABASE 
root.client_mtls_tree");
+      session.executeNonQueryStatement(
+          "CREATE TIMESERIES root.client_mtls_tree.d1.s1 WITH DATATYPE=INT32, 
ENCODING=PLAIN");
+      session.executeNonQueryStatement(
+          "INSERT INTO root.client_mtls_tree.d1(time, s1) VALUES (1, 11)");
+
+      try (SessionDataSet dataSet =
+          session.executeQueryStatement("SELECT s1 FROM 
root.client_mtls_tree.d1")) {
+        assertTrue(dataSet.hasNext());
+        final RowRecord record = dataSet.next();
+        assertEquals(1L, record.getTimestamp());
+        assertEquals(11, record.getFields().get(0).getIntV());
+        assertFalse(dataSet.hasNext());
+      }
+    }
+  }
+
+  @Test
+  public void sessionPoolCanConnectWithMutualSSL() throws Exception {
+    final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+    final ISessionPool pool =
+        new SessionPool.Builder()
+            .nodeUrls(Collections.singletonList(dataNode.getIpAndPortString()))
+            .maxSize(1)
+            .useSSL(true)
+            .trustStore(trustStorePath())
+            .trustStorePwd(STORE_PASSWORD)
+            .keyStore(keyStorePath())
+            .keyStorePwd(STORE_PASSWORD)
+            .build();
+    try {
+      pool.executeNonQueryStatement("CREATE DATABASE root.client_mtls_pool");
+      pool.executeNonQueryStatement(
+          "CREATE TIMESERIES root.client_mtls_pool.d1.s1 WITH DATATYPE=INT32, 
ENCODING=PLAIN");
+      pool.executeNonQueryStatement(
+          "INSERT INTO root.client_mtls_pool.d1(time, s1) VALUES (1, 22)");
+
+      try (SessionDataSetWrapper dataSet =
+          pool.executeQueryStatement("SELECT s1 FROM 
root.client_mtls_pool.d1")) {
+        assertTrue(dataSet.hasNext());
+        final RowRecord record = dataSet.next();
+        assertEquals(1L, record.getTimestamp());
+        assertEquals(22, record.getFields().get(0).getIntV());
+        assertFalse(dataSet.hasNext());
+      }
+    } finally {
+      pool.close();
+    }
+  }
+
+  @Test
+  public void tableSessionCanConnectWithMutualSSL() throws Exception {
+    try (ITableSession session = newMutualSSLTableSession()) {
+      session.executeNonQueryStatement("CREATE DATABASE IF NOT EXISTS 
client_mtls_table");
+      session.executeNonQueryStatement("USE client_mtls_table");
+      session.executeNonQueryStatement(
+          "CREATE TABLE IF NOT EXISTS mtls_table (tag1 STRING TAG, value INT32 
FIELD)");
+      session.executeNonQueryStatement(
+          "INSERT INTO mtls_table(time, tag1, value) VALUES (1, 'tag1', 33)");
+
+      try (SessionDataSet dataSet =
+          session.executeQueryStatement("SELECT time, value FROM mtls_table 
WHERE tag1 = 'tag1'")) {
+        assertTrue(dataSet.hasNext());
+        final RowRecord record = dataSet.next();
+        assertEquals(1L, record.getFields().get(0).getLongV());
+        assertEquals(33, record.getFields().get(1).getIntV());
+        assertFalse(dataSet.hasNext());
+      }
+    }
+  }
+
+  @Test
+  public void jdbcCanConnectWithMutualSSL() throws Exception {
+    final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                Config.IOTDB_URL_PREFIX + dataNode.getIpAndPortString(), 
mutualSSLProperties());
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE root.client_mtls_jdbc");
+      statement.execute(
+          "CREATE TIMESERIES root.client_mtls_jdbc.d1.s1 WITH DATATYPE=INT32, 
ENCODING=PLAIN");
+      statement.execute("INSERT INTO root.client_mtls_jdbc.d1(time, s1) VALUES 
(1, 44)");
+
+      try (ResultSet resultSet =
+          statement.executeQuery("SELECT s1 FROM root.client_mtls_jdbc.d1")) {
+        assertTrue(resultSet.next());
+        assertEquals(1L, resultSet.getLong(1));
+        assertEquals(44, resultSet.getInt(2));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  private static ISession newMutualSSLSession() throws 
IoTDBConnectionException {
+    final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+    final Session session =
+        new Session.Builder()
+            .host(dataNode.getIp())
+            .port(dataNode.getPort())
+            .useSSL(true)
+            .trustStore(trustStorePath())
+            .trustStorePwd(STORE_PASSWORD)
+            .keyStore(keyStorePath())
+            .keyStorePwd(STORE_PASSWORD)
+            .build();
+    session.open();
+    return session;
+  }
+
+  private static ITableSession newMutualSSLTableSession() throws 
IoTDBConnectionException {
+    final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+    return new TableSessionBuilder()
+        .nodeUrls(Collections.singletonList(dataNode.getIpAndPortString()))
+        .useSSL(true)
+        .trustStore(trustStorePath())
+        .trustStorePwd(STORE_PASSWORD)
+        .keyStore(keyStorePath())
+        .keyStorePwd(STORE_PASSWORD)
+        .build();
+  }
+
+  private static Properties mutualSSLProperties() {
+    final Properties properties = new Properties();
+    properties.put("user", SessionConfig.DEFAULT_USER);
+    properties.put("password", SessionConfig.DEFAULT_PASSWORD);
+    properties.put(Config.USE_SSL, Boolean.TRUE.toString());
+    properties.put(Config.TRUST_STORE, trustStorePath());
+    properties.put(Config.TRUST_STORE_PWD, STORE_PASSWORD);
+    properties.put(Config.KEY_STORE, keyStorePath());
+    properties.put(Config.KEY_STORE_PWD, STORE_PASSWORD);
+    return properties;
+  }
+
+  private void deleteTreeDatabase(final ISession session, final String 
database) {
+    try {
+      session.executeNonQueryStatement("DELETE DATABASE " + database);
+    } catch (Exception ignored) {
+      // ignored
+    }
+  }
+
+  private static String keyStorePath() {
+    return keyDir + "test-keystore";
+  }
+
+  private static String trustStorePath() {
+    return keyDir + "test-truststore";
+  }
+}
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
index 017f0a1a6ca..088131c2570 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
@@ -74,8 +74,10 @@ public abstract class AbstractCli {
 
   static final String USE_SSL_ARGS = "usessl";
   static final String TRUST_STORE_ARGS = "ts";
+  static final String KEY_STORE_ARGS = "ks";
 
   static final String TRUST_STORE_PWD_ARGS = "tpw";
+  static final String KEY_STORE_PWD_ARGS = "kpw";
 
   static final String SSL_PROTOCOL_ARGS = "ssl_protocol";
 
@@ -83,8 +85,10 @@ public abstract class AbstractCli {
 
   private static final String USE_SSL = "use_ssl";
   private static final String TRUST_STORE = "trust_store";
+  private static final String KEY_STORE = "key_store";
 
   private static final String TRUST_STORE_PWD = "trust_store_pwd";
+  private static final String KEY_STORE_PWD = "key_store_pwd";
   private static final String SSL_PROTOCOL = "ssl_protocol";
   private static final String NULL = "null";
 
@@ -135,6 +139,8 @@ public abstract class AbstractCli {
   static String trustStore;
   // TODO: Make non-static
   static String trustStorePwd;
+  static String keyStore;
+  static String keyStorePwd;
   static String sslProtocol;
 
   static String execute;
@@ -160,6 +166,8 @@ public abstract class AbstractCli {
     keywordSet.add("-" + USE_SSL_ARGS);
     keywordSet.add("-" + TRUST_STORE_ARGS);
     keywordSet.add("-" + TRUST_STORE_PWD_ARGS);
+    keywordSet.add("-" + KEY_STORE_ARGS);
+    keywordSet.add("-" + KEY_STORE_PWD_ARGS);
     keywordSet.add("-" + SSL_PROTOCOL_ARGS);
     keywordSet.add("-" + EXECUTE_ARGS);
     keywordSet.add("-" + ISO8601_ARGS);
@@ -219,6 +227,42 @@ public abstract class AbstractCli {
             .build();
     options.addOption(useSSL);
 
+    Option trustStore =
+        Option.builder(TRUST_STORE_ARGS)
+            .longOpt(TRUST_STORE)
+            .argName(TRUST_STORE)
+            .hasArg()
+            .desc("Trust store. (optional)")
+            .build();
+    options.addOption(trustStore);
+
+    Option trustStorePwd =
+        Option.builder(TRUST_STORE_PWD_ARGS)
+            .longOpt(TRUST_STORE_PWD)
+            .argName(TRUST_STORE_PWD)
+            .hasArg()
+            .desc("Trust store password. (optional)")
+            .build();
+    options.addOption(trustStorePwd);
+
+    Option keyStore =
+        Option.builder(KEY_STORE_ARGS)
+            .longOpt(KEY_STORE)
+            .argName(KEY_STORE)
+            .hasArg()
+            .desc("Key store for mutual SSL. (optional)")
+            .build();
+    options.addOption(keyStore);
+
+    Option keyStorePwd =
+        Option.builder(KEY_STORE_PWD_ARGS)
+            .longOpt(KEY_STORE_PWD)
+            .argName(KEY_STORE_PWD)
+            .hasArg()
+            .desc("Key store password for mutual SSL. (optional)")
+            .build();
+    options.addOption(keyStorePwd);
+
     Option sslProtocol =
         Option.builder(SSL_PROTOCOL_ARGS)
             .longOpt(SSL_PROTOCOL)
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
index a9e911be6ed..63c3c5d1153 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
@@ -111,6 +111,12 @@ public class Cli extends AbstractCli {
       info.setProperty("use_ssl", useSsl);
       info.setProperty("trust_store", trustStore);
       info.setProperty("trust_store_pwd", trustStorePwd);
+      if (keyStore != null) {
+        info.setProperty(Config.KEY_STORE, keyStore);
+      }
+      if (keyStorePwd != null) {
+        info.setProperty(Config.KEY_STORE_PWD, keyStorePwd);
+      }
       if (sslProtocol != null) {
         info.setProperty(Config.SSL_PROTOCOL, sslProtocol);
       }
@@ -164,8 +170,21 @@ public class Cli extends AbstractCli {
       useSsl = commandLine.getOptionValue(USE_SSL_ARGS);
       sslProtocol = commandLine.getOptionValue(SSL_PROTOCOL_ARGS);
       if (Boolean.parseBoolean(useSsl)) {
-        trustStore = ctx.getLineReader().readLine("please input your 
trust_store:", '\0');
-        trustStorePwd = ctx.getLineReader().readLine("please input your 
trust_store_pwd:", '\0');
+        trustStore = commandLine.getOptionValue(TRUST_STORE_ARGS);
+        if (trustStore == null) {
+          trustStore = ctx.getLineReader().readLine("please input your 
trust_store:", '\0');
+        }
+        trustStorePwd = commandLine.getOptionValue(TRUST_STORE_PWD_ARGS);
+        if (trustStorePwd == null) {
+          trustStorePwd = ctx.getLineReader().readLine("please input your 
trust_store_pwd:", '\0');
+        }
+        keyStore = commandLine.getOptionValue(KEY_STORE_ARGS);
+        keyStorePwd = commandLine.getOptionValue(KEY_STORE_PWD_ARGS);
+        if (keyStore != null && keyStorePwd == null) {
+          keyStorePwd = ctx.getLineReader().readLine("please input your 
key_store_pwd:", '\0');
+        } else if (keyStore == null && keyStorePwd != null) {
+          keyStore = ctx.getLineReader().readLine("please input your 
key_store:", '\0');
+        }
       }
       password = commandLine.getOptionValue(PW_ARGS);
       if (password == null) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
index a1063260957..ed2d96dc860 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
@@ -68,6 +68,14 @@ public class Constants {
   public static final String TRUST_STORE_PWD_NAME = "trust_store_password";
   public static final String TRUST_STORE_PWD_DESC = "Trust store password. 
(optional)";
 
+  public static final String KEY_STORE_ARGS = "ks";
+  public static final String KEY_STORE_NAME = "key_store";
+  public static final String KEY_STORE_DESC = "Key store for mutual SSL. 
(optional)";
+
+  public static final String KEY_STORE_PWD_ARGS = "kpw";
+  public static final String KEY_STORE_PWD_NAME = "key_store_password";
+  public static final String KEY_STORE_PWD_DESC = "Key store password for 
mutual SSL. (optional)";
+
   public static final String SSL_PROTOCOL_ARGS = "ssl_protocol";
   public static final String SSL_PROTOCOL_NAME = "ssl_protocol";
   public static final String SSL_PROTOCOL_DESC = "SSL protocol. (optional)";
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
index 1f79b303f6e..e7ca2317be6 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
@@ -133,6 +133,26 @@ public class OptionsUtil extends Constants {
             .build();
     options.addOption(opTrustStorePwd);
 
+    Option opKeyStore =
+        Option.builder(KEY_STORE_ARGS)
+            .longOpt(KEY_STORE_NAME)
+            .optionalArg(true)
+            .argName(KEY_STORE_NAME)
+            .hasArg()
+            .desc(KEY_STORE_DESC)
+            .build();
+    options.addOption(opKeyStore);
+
+    Option opKeyStorePwd =
+        Option.builder(KEY_STORE_PWD_ARGS)
+            .longOpt(KEY_STORE_PWD_NAME)
+            .optionalArg(true)
+            .argName(KEY_STORE_PWD_NAME)
+            .hasArg()
+            .desc(KEY_STORE_PWD_DESC)
+            .build();
+    options.addOption(opKeyStorePwd);
+
     Option opSslProtocol =
         Option.builder(SSL_PROTOCOL_ARGS)
             .longOpt(SSL_PROTOCOL_NAME)
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
index 6ea79a5b582..07a21109c20 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
@@ -97,6 +97,8 @@ public abstract class AbstractDataTool {
   protected static Boolean useSsl;
   protected static String trustStore;
   protected static String trustStorePwd;
+  protected static String keyStore;
+  protected static String keyStorePwd;
   protected static String sslProtocol;
   protected static Boolean aligned;
   protected static String database;
@@ -140,6 +142,9 @@ public abstract class AbstractDataTool {
 
   protected static Session.Builder configureSsl(Session.Builder builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -148,6 +153,9 @@ public abstract class AbstractDataTool {
 
   protected static SessionPool.Builder configureSsl(SessionPool.Builder 
builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -156,6 +164,9 @@ public abstract class AbstractDataTool {
 
   protected static TableSessionBuilder configureSsl(TableSessionBuilder 
builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -164,6 +175,9 @@ public abstract class AbstractDataTool {
 
   protected static TableSessionPoolBuilder 
configureSsl(TableSessionPoolBuilder builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -219,6 +233,13 @@ public abstract class AbstractDataTool {
       } else {
         trustStorePwd = cliCtx.getLineReader().readLine("please input your 
trust_store_pwd:", '\0');
       }
+      keyStore = commandLine.getOptionValue(Constants.KEY_STORE_ARGS);
+      keyStorePwd = commandLine.getOptionValue(Constants.KEY_STORE_PWD_ARGS);
+      if (keyStore != null && keyStorePwd == null) {
+        keyStorePwd = cliCtx.getLineReader().readLine("please input your 
key_store_pwd:", '\0');
+      } else if (keyStore == null && keyStorePwd != null) {
+        keyStore = cliCtx.getLineReader().readLine("please input your 
key_store:", '\0');
+      }
     }
     boolean hasPw = commandLine.hasOption(Constants.PW_ARGS);
     if (hasPw) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
index 2bee5f5d331..6f0e533065f 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
@@ -55,6 +55,8 @@ public abstract class AbstractSchemaTool {
   protected static Boolean useSsl;
   protected static String trustStore;
   protected static String trustStorePwd;
+  protected static String keyStore;
+  protected static String keyStorePwd;
   protected static String sslProtocol;
   protected static Session session;
   protected static String queryPath;
@@ -78,6 +80,9 @@ public abstract class AbstractSchemaTool {
 
   protected static Session.Builder configureSsl(Session.Builder builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -86,6 +91,9 @@ public abstract class AbstractSchemaTool {
 
   protected static SessionPool.Builder configureSsl(SessionPool.Builder 
builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -94,6 +102,9 @@ public abstract class AbstractSchemaTool {
 
   protected static TableSessionPoolBuilder 
configureSsl(TableSessionPoolBuilder builder) {
     builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+    if (keyStore != null) {
+      builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+    }
     if (sslProtocol != null) {
       builder.sslProtocol(sslProtocol);
     }
@@ -147,6 +158,13 @@ public abstract class AbstractSchemaTool {
       } else {
         trustStorePwd = cliCtx.getLineReader().readLine("please input your 
trust_store_pwd:", '\0');
       }
+      keyStore = commandLine.getOptionValue(Constants.KEY_STORE_ARGS);
+      keyStorePwd = commandLine.getOptionValue(Constants.KEY_STORE_PWD_ARGS);
+      if (keyStore != null && keyStorePwd == null) {
+        keyStorePwd = cliCtx.getLineReader().readLine("please input your 
key_store_pwd:", '\0');
+      } else if (keyStore == null && keyStorePwd != null) {
+        keyStore = cliCtx.getLineReader().readLine("please input your 
key_store:", '\0');
+      }
     }
     boolean hasPw = commandLine.hasOption(Constants.PW_ARGS);
     if (hasPw) {
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java 
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
index d79656cc8d2..54aca784d15 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
@@ -82,6 +82,10 @@ public class Config {
 
   public static final String TRUST_STORE_PWD = "trust_store_pwd";
 
+  public static final String KEY_STORE = "key_store";
+
+  public static final String KEY_STORE_PWD = "key_store_pwd";
+
   public static final String SSL_PROTOCOL = "ssl_protocol";
 
   static final String DEFAULT_SSL_PROTOCOL = "TLS";
diff --git 
a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java 
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
index 0dc81115dd0..9a6dc91ceb4 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
@@ -544,12 +544,14 @@ public class IoTDBConnection implements Connection {
 
     if (params.isUseSSL()) {
       transport =
-          DeepCopyRpcTransportFactory.INSTANCE.getTransportWithSSLConfig(
+          DeepCopyRpcTransportFactory.INSTANCE.getTransport(
               params.getHost(),
               params.getPort(),
               getNetworkTimeout(),
               params.getTrustStore(),
               params.getTrustStorePwd(),
+              params.getKeyStore(),
+              params.getKeyStorePwd(),
               params.getSslProtocol());
     } else {
       transport =
diff --git 
a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
 
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
index 8bf51379c5c..6c9a148517c 100644
--- 
a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
+++ 
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
@@ -51,6 +51,8 @@ public class IoTDBConnectionParams {
   private boolean useSSL = false;
   private String trustStore;
   private String trustStorePwd;
+  private String keyStore;
+  private String keyStorePwd;
   private String sslProtocol = Config.DEFAULT_SSL_PROTOCOL;
 
   private String sqlDialect = TREE;
@@ -185,6 +187,22 @@ public class IoTDBConnectionParams {
     this.trustStorePwd = trustStorePwd;
   }
 
+  public String getKeyStore() {
+    return keyStore;
+  }
+
+  public void setKeyStore(String keyStore) {
+    this.keyStore = keyStore;
+  }
+
+  public String getKeyStorePwd() {
+    return keyStorePwd;
+  }
+
+  public void setKeyStorePwd(String keyStorePwd) {
+    this.keyStorePwd = keyStorePwd;
+  }
+
   public String getSslProtocol() {
     return sslProtocol;
   }
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java 
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
index ea205ef7267..cf4fcb7d056 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
@@ -138,6 +138,12 @@ public class Utils {
     if (info.containsKey(Config.TRUST_STORE_PWD)) {
       params.setTrustStorePwd(info.getProperty(Config.TRUST_STORE_PWD));
     }
+    if (info.containsKey(Config.KEY_STORE)) {
+      params.setKeyStore(info.getProperty(Config.KEY_STORE));
+    }
+    if (info.containsKey(Config.KEY_STORE_PWD)) {
+      params.setKeyStorePwd(info.getProperty(Config.KEY_STORE_PWD));
+    }
     if (info.containsKey(Config.SSL_PROTOCOL)) {
       
params.setSslProtocol(RpcSslUtils.normalizeProtocol(info.getProperty(Config.SSL_PROTOCOL)));
     }
diff --git 
a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java 
b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
index d201a9b2d45..0787886c03a 100644
--- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
+++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
@@ -163,11 +163,19 @@ public class UtilsTest {
   @Test
   public void testParseSslConfig() throws IoTDBURLException {
     Properties properties = new Properties();
+    properties.setProperty(Config.TRUST_STORE, "/tmp/truststore.p12");
+    properties.setProperty(Config.TRUST_STORE_PWD, "trust_pass");
+    properties.setProperty(Config.KEY_STORE, "/tmp/keystore.p12");
+    properties.setProperty(Config.KEY_STORE_PWD, "key_pass");
     IoTDBConnectionParams params =
         Utils.parseUrl(
             
"jdbc:iotdb://127.0.0.1:6667?use_ssl=true&ssl_protocol=ProviderProtocol", 
properties);
 
     assertTrue(params.isUseSSL());
     assertEquals("ProviderProtocol", params.getSslProtocol());
+    assertEquals("/tmp/truststore.p12", params.getTrustStore());
+    assertEquals("trust_pass", params.getTrustStorePwd());
+    assertEquals("/tmp/keystore.p12", params.getKeyStore());
+    assertEquals("key_pass", params.getKeyStorePwd());
   }
 }
diff --git 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
index 03bf2070bca..57f1dd5a894 100644
--- 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
+++ 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
@@ -19,8 +19,6 @@
 
 package org.apache.iotdb.rpc;
 
-import org.apache.iotdb.rpc.i18n.RpcMessages;
-
 import org.apache.thrift.transport.TMemoryInputTransport;
 import org.apache.thrift.transport.TSSLTransportFactory;
 import org.apache.thrift.transport.TSocket;
@@ -28,10 +26,6 @@ import org.apache.thrift.transport.TTransport;
 import org.apache.thrift.transport.TTransportException;
 import org.apache.thrift.transport.TTransportFactory;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-
 @SuppressWarnings("java:S1135") // ignore todos
 public class BaseRpcTransportFactory extends TTransportFactory {
 
@@ -120,11 +114,9 @@ public class BaseRpcTransportFactory extends 
TTransportFactory {
       throws TTransportException {
     TSSLTransportFactory.TSSLTransportParameters params =
         RpcSslUtils.createTSSLTransportParameters(sslProtocol);
-    if (Files.exists(Paths.get(trustStore)) && 
Files.exists(Paths.get(keyStore))) {
-      RpcSslUtils.setTrustStore(params, trustStore, trustStorePwd);
+    RpcSslUtils.setTrustStore(params, trustStore, trustStorePwd);
+    if (hasText(keyStore)) {
       RpcSslUtils.setKeyStore(params, keyStore, keyStorePwd);
-    } else {
-      throw new TTransportException(new 
IOException(RpcMessages.COULD_NOT_LOAD_KEYSTORE));
     }
     TTransport transport = TSSLTransportFactory.getClientSocket(ip, port, 
timeout, params);
     return inner.getTransport(transport);
@@ -150,4 +142,8 @@ public class BaseRpcTransportFactory extends 
TTransportFactory {
   public static void setThriftMaxFrameSize(int thriftMaxFrameSize) {
     BaseRpcTransportFactory.thriftMaxFrameSize = thriftMaxFrameSize;
   }
+
+  private static boolean hasText(String value) {
+    return value != null && !value.trim().isEmpty();
+  }
 }
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
index 873e6dd248d..44500ed3f10 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
@@ -63,6 +63,8 @@ public abstract class AbstractSessionBuilder {
   public boolean useSSL = false;
   public String trustStore;
   public String trustStorePwd;
+  public String keyStore;
+  public String keyStorePwd;
   public String sslProtocol = SessionConfig.DEFAULT_SSL_PROTOCOL;
 
   // max retry count, if set to 0, means that we won't do any retry
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
index a41db581e9c..c1f43fa30d4 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
@@ -61,6 +61,8 @@ public class NodesSupplier implements INodeSupplier, Runnable 
{
   private final boolean useSSL;
   private final String trustStore;
   private final String trustStorePwd;
+  private final String keyStore;
+  private final String keyStorePwd;
   private final String sslProtocol;
   private final boolean enableRPCCompression;
   private final String userName;
@@ -96,6 +98,8 @@ public class NodesSupplier implements INodeSupplier, Runnable 
{
       boolean useSSL,
       String trustStore,
       String trustStorePwd,
+      String keyStore,
+      String keyStorePwd,
       String sslProtocol,
       boolean enableRPCCompression,
       String version) {
@@ -112,6 +116,8 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
             useSSL,
             trustStore,
             trustStorePwd,
+            keyStore,
+            keyStorePwd,
             sslProtocol,
             enableRPCCompression,
             version);
@@ -135,6 +141,8 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
       boolean useSSL,
       String trustStore,
       String trustStorePwd,
+      String keyStore,
+      String keyStorePwd,
       String sslProtocol,
       boolean enableRPCCompression,
       String version) {
@@ -144,6 +152,8 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
     this.useSSL = useSSL;
     this.trustStore = trustStore;
     this.trustStorePwd = trustStorePwd;
+    this.keyStore = keyStore;
+    this.keyStorePwd = keyStorePwd;
     this.sslProtocol = sslProtocol;
     this.enableRPCCompression = enableRPCCompression;
     this.zoneId = zoneId == null ? ZoneId.systemDefault() : zoneId;
@@ -193,6 +203,8 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
           useSSL,
           trustStore,
           trustStorePwd,
+          keyStore,
+          keyStorePwd,
           sslProtocol,
           userName,
           password,
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
index 648a63845b4..8d182122de5 100644
--- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
+++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
@@ -134,6 +134,8 @@ public class Session implements ISession {
   protected boolean useSSL;
   protected String trustStore;
   protected String trustStorePwd;
+  protected String keyStore;
+  protected String keyStorePwd;
   protected String sslProtocol;
 
   /**
@@ -475,6 +477,8 @@ public class Session implements ISession {
     this.useSSL = builder.useSSL;
     this.trustStore = builder.trustStore;
     this.trustStorePwd = builder.trustStorePwd;
+    this.keyStore = builder.keyStore;
+    this.keyStorePwd = builder.keyStorePwd;
     this.sslProtocol = builder.sslProtocol;
     this.enableAutoFetch = builder.enableAutoFetch;
     this.maxRetryCount = builder.maxRetryCount;
@@ -545,6 +549,8 @@ public class Session implements ISession {
               useSSL,
               trustStore,
               trustStorePwd,
+              keyStore,
+              keyStorePwd,
               sslProtocol,
               enableRPCCompaction,
               version.toString());
@@ -4438,6 +4444,16 @@ public class Session implements ISession {
       return this;
     }
 
+    public Builder keyStore(String keyStore) {
+      this.keyStore = keyStore;
+      return this;
+    }
+
+    public Builder keyStorePwd(String keyStorePwd) {
+      this.keyStorePwd = keyStorePwd;
+      return this;
+    }
+
     public Builder sslProtocol(String sslProtocol) {
       this.sslProtocol = sslProtocol;
       return this;
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
index 468b59abb0c..ec91fc9f8c0 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
@@ -153,7 +153,13 @@ public class SessionConnection {
     this.database = database;
     try {
       init(
-          endPoint, session.useSSL, session.trustStore, session.trustStorePwd, 
session.sslProtocol);
+          endPoint,
+          session.useSSL,
+          session.trustStore,
+          session.trustStorePwd,
+          session.keyStore,
+          session.keyStorePwd,
+          session.sslProtocol);
     } catch (StatementExecutionException e) {
       throw new IoTDBConnectionException(e.getMessage());
     } catch (IoTDBConnectionException e) {
@@ -186,6 +192,8 @@ public class SessionConnection {
       boolean useSSL,
       String trustStore,
       String trustStorePwd,
+      String keyStore,
+      String keyStorePwd,
       String sslProtocol)
       throws IoTDBConnectionException, StatementExecutionException {
     
DeepCopyRpcTransportFactory.setDefaultBufferCapacity(session.thriftDefaultBufferSize);
@@ -196,12 +204,14 @@ public class SessionConnection {
       }
       if (useSSL) {
         transport =
-            DeepCopyRpcTransportFactory.INSTANCE.getTransportWithSSLConfig(
+            DeepCopyRpcTransportFactory.INSTANCE.getTransport(
                 endPoint.getIp(),
                 endPoint.getPort(),
                 session.connectionTimeoutInMs,
                 trustStore,
                 trustStorePwd,
+                keyStore,
+                keyStorePwd,
                 sslProtocol);
       } else {
         transport =
@@ -278,6 +288,8 @@ public class SessionConnection {
             session.useSSL,
             session.trustStore,
             session.trustStorePwd,
+            session.keyStore,
+            session.keyStorePwd,
             session.sslProtocol);
       } catch (IoTDBConnectionException e) {
         if (!reconnect()) {
@@ -1100,6 +1112,8 @@ public class SessionConnection {
                 session.useSSL,
                 session.trustStore,
                 session.trustStorePwd,
+                session.keyStore,
+                session.keyStorePwd,
                 session.sslProtocol);
             connectedSuccess = true;
           } catch (IoTDBConnectionException e) {
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
index e724c0fd532..2dca0d351dc 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
@@ -239,6 +239,30 @@ public class TableSessionBuilder extends 
AbstractSessionBuilder {
     return this;
   }
 
+  /**
+   * Sets the key store path for mutual SSL connections.
+   *
+   * @param keyStore the key store path.
+   * @return the current {@link TableSessionBuilder} instance.
+   * @defaultValue null
+   */
+  public TableSessionBuilder keyStore(String keyStore) {
+    this.keyStore = keyStore;
+    return this;
+  }
+
+  /**
+   * Sets the key store password for mutual SSL connections.
+   *
+   * @param keyStorePwd the key store password.
+   * @return the current {@link TableSessionBuilder} instance.
+   * @defaultValue null
+   */
+  public TableSessionBuilder keyStorePwd(String keyStorePwd) {
+    this.keyStorePwd = keyStorePwd;
+    return this;
+  }
+
   /**
    * Sets the SSL protocol for secure connections.
    *
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
index 3ab3abd581d..2a7f970bd50 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
@@ -78,6 +78,8 @@ public class ThriftConnection {
       boolean useSSL,
       String trustStore,
       String trustStorePwd,
+      String keyStore,
+      String keyStorePwd,
       String sslProtocol,
       String username,
       String password,
@@ -90,12 +92,14 @@ public class ThriftConnection {
     try {
       if (useSSL) {
         transport =
-            DeepCopyRpcTransportFactory.INSTANCE.getTransportWithSSLConfig(
+            DeepCopyRpcTransportFactory.INSTANCE.getTransport(
                 endPoint.getIp(),
                 endPoint.getPort(),
                 connectionTimeoutInMs,
                 trustStore,
                 trustStorePwd,
+                keyStore,
+                keyStorePwd,
                 sslProtocol);
       } else {
         transport =
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
index 8136b2be68f..3a9c56a5e50 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
@@ -117,6 +117,10 @@ public class SessionPool implements ISessionPool {
 
   private String trustStorePwd;
 
+  private String keyStore;
+
+  private String keyStorePwd;
+
   private String sslProtocol = SessionConfig.DEFAULT_SSL_PROTOCOL;
 
   private ZoneId zoneId;
@@ -540,6 +544,8 @@ public class SessionPool implements ISessionPool {
     this.useSSL = builder.useSSL;
     this.trustStore = builder.trustStore;
     this.trustStorePwd = builder.trustStorePwd;
+    this.keyStore = builder.keyStore;
+    this.keyStorePwd = builder.keyStorePwd;
     this.sslProtocol = builder.sslProtocol;
     this.maxRetryCount = builder.maxRetryCount;
     this.retryIntervalInMs = builder.retryIntervalInMs;
@@ -598,6 +604,8 @@ public class SessionPool implements ISessionPool {
               .useSSL(useSSL)
               .trustStore(trustStore)
               .trustStorePwd(trustStorePwd)
+              .keyStore(keyStore)
+              .keyStorePwd(keyStorePwd)
               .sslProtocol(sslProtocol)
               .maxRetryCount(maxRetryCount)
               .retryIntervalInMs(retryIntervalInMs)
@@ -624,6 +632,8 @@ public class SessionPool implements ISessionPool {
               .useSSL(useSSL)
               .trustStore(trustStore)
               .trustStorePwd(trustStorePwd)
+              .keyStore(keyStore)
+              .keyStorePwd(keyStorePwd)
               .sslProtocol(sslProtocol)
               .maxRetryCount(maxRetryCount)
               .retryIntervalInMs(retryIntervalInMs)
@@ -669,6 +679,8 @@ public class SessionPool implements ISessionPool {
             useSSL,
             trustStore,
             trustStorePwd,
+            keyStore,
+            keyStorePwd,
             sslProtocol,
             enableThriftCompression,
             version.toString());
@@ -3645,6 +3657,16 @@ public class SessionPool implements ISessionPool {
       return this;
     }
 
+    public Builder keyStore(String keyStore) {
+      this.keyStore = keyStore;
+      return this;
+    }
+
+    public Builder keyStorePwd(String keyStorePwd) {
+      this.keyStorePwd = keyStorePwd;
+      return this;
+    }
+
     public Builder sslProtocol(String sslProtocol) {
       this.sslProtocol = sslProtocol;
       return this;
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
index 2c7aba1a452..ab3e1f1c6a6 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
@@ -281,6 +281,30 @@ public class TableSessionPoolBuilder extends 
AbstractSessionPoolBuilder {
     return this;
   }
 
+  /**
+   * Sets the key store path for mutual SSL connections.
+   *
+   * @param keyStore the key store path.
+   * @return the current {@link TableSessionPoolBuilder} instance.
+   * @defaultValue null
+   */
+  public TableSessionPoolBuilder keyStore(String keyStore) {
+    this.keyStore = keyStore;
+    return this;
+  }
+
+  /**
+   * Sets the key store password for mutual SSL connections.
+   *
+   * @param keyStorePwd the key store password.
+   * @return the current {@link TableSessionPoolBuilder} instance.
+   * @defaultValue null
+   */
+  public TableSessionPoolBuilder keyStorePwd(String keyStorePwd) {
+    this.keyStorePwd = keyStorePwd;
+    return this;
+  }
+
   /**
    * Sets the SSL protocol for secure connections.
    *
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
index dcc8690de8a..89650657a61 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
@@ -65,32 +65,56 @@ public class ExternalRPCService extends ThriftService 
implements ExternalRPCServ
   @Override
   public void initThriftServiceThread() throws IllegalAccessException {
     try {
-      thriftServiceThread =
-          commonConfig.isEnableThriftClientSSL()
-              ? new ThriftServiceThread(
-                  processor,
-                  getID().getName(),
-                  ThreadName.CLIENT_RPC_PROCESSOR.getName(),
-                  getBindIP(),
-                  getBindPort(),
-                  config.getRpcMaxConcurrentClientNum(),
-                  config.getThriftServerAwaitTimeForStopService(),
-                  new RPCServiceThriftHandler(impl),
-                  config.isRpcThriftCompressionEnable(),
-                  commonConfig.getKeyStorePath(),
-                  commonConfig.getKeyStorePwd(),
-                  ZeroCopyRpcTransportFactory.INSTANCE)
-              : new ThriftServiceThread(
-                  processor,
-                  getID().getName(),
-                  ThreadName.CLIENT_RPC_PROCESSOR.getName(),
-                  getBindIP(),
-                  getBindPort(),
-                  config.getRpcMaxConcurrentClientNum(),
-                  config.getThriftServerAwaitTimeForStopService(),
-                  new RPCServiceThriftHandler(impl),
-                  config.isRpcThriftCompressionEnable(),
-                  ZeroCopyRpcTransportFactory.INSTANCE);
+      if (!commonConfig.isEnableThriftClientSSL()) {
+        thriftServiceThread =
+            new ThriftServiceThread(
+                processor,
+                getID().getName(),
+                ThreadName.CLIENT_RPC_PROCESSOR.getName(),
+                getBindIP(),
+                getBindPort(),
+                config.getRpcMaxConcurrentClientNum(),
+                config.getThriftServerAwaitTimeForStopService(),
+                new RPCServiceThriftHandler(impl),
+                config.isRpcThriftCompressionEnable(),
+                ZeroCopyRpcTransportFactory.INSTANCE);
+      } else if (commonConfig.isThriftSSLClientAuth()) {
+        if (!hasText(commonConfig.getTrustStorePath())) {
+          throw new IllegalAccessException(
+              "trust_store_path must be set when thrift_ssl_client_auth is 
true");
+        }
+        thriftServiceThread =
+            new ThriftServiceThread(
+                processor,
+                getID().getName(),
+                ThreadName.CLIENT_RPC_PROCESSOR.getName(),
+                getBindIP(),
+                getBindPort(),
+                config.getRpcMaxConcurrentClientNum(),
+                config.getThriftServerAwaitTimeForStopService(),
+                new RPCServiceThriftHandler(impl),
+                config.isRpcThriftCompressionEnable(),
+                commonConfig.getKeyStorePath(),
+                commonConfig.getKeyStorePwd(),
+                commonConfig.getTrustStorePath(),
+                commonConfig.getTrustStorePwd(),
+                ZeroCopyRpcTransportFactory.INSTANCE);
+      } else {
+        thriftServiceThread =
+            new ThriftServiceThread(
+                processor,
+                getID().getName(),
+                ThreadName.CLIENT_RPC_PROCESSOR.getName(),
+                getBindIP(),
+                getBindPort(),
+                config.getRpcMaxConcurrentClientNum(),
+                config.getThriftServerAwaitTimeForStopService(),
+                new RPCServiceThriftHandler(impl),
+                config.isRpcThriftCompressionEnable(),
+                commonConfig.getKeyStorePath(),
+                commonConfig.getKeyStorePwd(),
+                ZeroCopyRpcTransportFactory.INSTANCE);
+      }
     } catch (RPCServiceException e) {
       throw new IllegalAccessException(e.getMessage());
     }
@@ -118,6 +142,10 @@ public class ExternalRPCService extends ThriftService 
implements ExternalRPCServ
     return getBindPort();
   }
 
+  private boolean hasText(String value) {
+    return value != null && !value.trim().isEmpty();
+  }
+
   private static class RPCServiceHolder {
 
     private static final ExternalRPCService INSTANCE = new 
ExternalRPCService();
diff --git 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index 1e808d7d75a..6d75a72aaab 100644
--- 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++ 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -444,6 +444,13 @@ dn_metric_internal_reporter_type=MEMORY
 # Privilege: SECURITY
 enable_thrift_ssl=false
 
+# Whether client authentication is required for Thrift SSL connections.
+# This only takes effect when enable_thrift_ssl=true.
+# effectiveMode: restart
+# Datatype: boolean
+# Privilege: SECURITY
+thrift_ssl_client_auth=false
+
 # Whether enable SSL for Rest Service
 # effectiveMode: restart
 # Datatype: boolean
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
index 53127fa2cdc..257b2a8ad51 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
@@ -490,6 +490,9 @@ public class CommonConfig {
   /** Enable the Thrift Client ssl. */
   private boolean enableThriftClientSSL = false;
 
+  /** Whether the external Thrift SSL service requires client certificate 
authentication. */
+  private boolean thriftSSLClientAuth = false;
+
   /** Enable the cluster internal connection ssl. */
   private boolean enableInternalSSL = false;
 
@@ -2998,6 +3001,14 @@ public class CommonConfig {
     this.enableThriftClientSSL = enableThriftClientSSL;
   }
 
+  public boolean isThriftSSLClientAuth() {
+    return thriftSSLClientAuth;
+  }
+
+  public void setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+    this.thriftSSLClientAuth = thriftSSLClientAuth;
+  }
+
   public boolean isEnableInternalSSL() {
     return enableInternalSSL;
   }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
index 824ec639ef6..56e8587eb07 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
@@ -679,6 +679,10 @@ public class CommonDescriptor {
         Boolean.parseBoolean(
             properties.getProperty(
                 "enable_thrift_ssl", 
Boolean.toString(config.isEnableThriftClientSSL()))));
+    config.setThriftSSLClientAuth(
+        Boolean.parseBoolean(
+            properties.getProperty(
+                "thrift_ssl_client_auth", 
Boolean.toString(config.isThriftSSLClientAuth()))));
     config.setKeyStorePath(properties.getProperty("key_store_path", 
config.getKeyStorePath()));
     config.setKeyStorePwd(properties.getProperty("key_store_pwd", 
config.getKeyStorePwd()));
     config.setTrustStorePath(

Reply via email to