You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by tf...@apache.org on 2023/11/30 21:59:18 UTC

(solr) branch main updated: SOLR-16743: Auto reload keystore/truststore on change (#2100)

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

tflobbe pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 9f21a71bbd8 SOLR-16743: Auto reload keystore/truststore on change (#2100)
9f21a71bbd8 is described below

commit 9f21a71bbd89fa61821850b64963062cc21256f8
Author: Tomas Eduardo Fernandez Lobbe <tf...@apache.org>
AuthorDate: Thu Nov 30 13:59:11 2023 -0800

    SOLR-16743: Auto reload keystore/truststore on change (#2100)
    
    Add Jetty module to scan and automatically reload key store and trust store changes. Also, add scanner to the SolrJ library to do the same on the client side if configured
---
 solr/CHANGES.txt                                   |   3 +
 solr/bin/solr                                      |  12 ++
 solr/bin/solr.cmd                                  |  28 ++++-
 solr/bin/solr.in.sh                                |   1 +
 .../src/java/org/apache/solr/cli/CreateTool.java   |   1 +
 .../src/java/org/apache/solr/cli/DeleteTool.java   |   1 +
 .../src/java/org/apache/solr/cli/PostLogsTool.java |   5 +-
 .../core/src/java/org/apache/solr/cli/SolrCLI.java |   1 +
 solr/packaging/test/test_ssl.bats                  | 121 ++++++++++++++++++++-
 solr/server/etc/jetty-ssl-context-reload.xml       |  13 +++
 solr/server/etc/security.policy                    |   3 +
 solr/server/modules/ssl-reload.mod                 |  12 ++
 .../deployment-guide/pages/enabling-ssl.adoc       |  17 +++
 .../solr/client/solrj/impl/Http2SolrClient.java    |  65 ++++++++++-
 14 files changed, 273 insertions(+), 10 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 3e8de72b190..fa799b81fdb 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -85,6 +85,9 @@ New Features
 
 * SOLR-17079: Allow to declare replica placement plugins in solr.xml  (Vincent Primault)
 
+* SOLR-16743: When using TLS, Solr can now auto-reload the keystore and truststore without the need to restart the process.
+  This is enabled by default when running with TLS and can be disabled or configured in solr.in.sh (Houston Putman, Tomás Fernández Löbbe)
+
 Improvements
 ---------------------
 * SOLR-17053: Distributed search with shards.tolerant: if all shards fail, fail the request (Aparna Suresh via David Smiley)
diff --git a/solr/bin/solr b/solr/bin/solr
index 8837f858d29..0ad6b8abcc5 100755
--- a/solr/bin/solr
+++ b/solr/bin/solr
@@ -208,9 +208,17 @@ if [ -z "${SOLR_SSL_ENABLED:-}" ]; then
 fi
 if [ "$SOLR_SSL_ENABLED" == "true" ]; then
   SOLR_JETTY_CONFIG+=("--module=https" "--lib=$DEFAULT_SERVER_DIR/solr-webapp/webapp/WEB-INF/lib/*")
+  if [ "${SOLR_SSL_RELOAD_ENABLED:-true}" == "true" ]; then
+    SOLR_JETTY_CONFIG+=("--module=ssl-reload")
+    SOLR_SSL_OPTS+=" -Dsolr.keyStoreReload.enabled=true"
+  fi
   SOLR_URL_SCHEME=https
   if [ -n "$SOLR_SSL_KEY_STORE" ]; then
     SOLR_SSL_OPTS+=" -Dsolr.jetty.keystore=$SOLR_SSL_KEY_STORE"
+    if [ "${SOLR_SSL_RELOAD_ENABLED:-true}" == "true" ] && [ "${SOLR_SECURITY_MANAGER_ENABLED:-true}" == "true"  ]; then
+      # In this case we need to allow reads from the parent directory of the keystore
+      SOLR_SSL_OPTS+=" -Dsolr.jetty.keystoreParentPath=$SOLR_SSL_KEY_STORE/.."
+    fi
   fi
   if [ -n "$SOLR_SSL_KEY_STORE_PASSWORD" ]; then
     export SOLR_SSL_KEY_STORE_PASSWORD=$SOLR_SSL_KEY_STORE_PASSWORD
@@ -249,6 +257,10 @@ if [ "$SOLR_SSL_ENABLED" == "true" ]; then
     if [ -n "$SOLR_SSL_CLIENT_KEY_STORE_TYPE" ]; then
       SOLR_SSL_OPTS+=" -Djavax.net.ssl.keyStoreType=$SOLR_SSL_CLIENT_KEY_STORE_TYPE"
     fi
+    if [ "${SOLR_SSL_RELOAD_ENABLED:-true}" == "true" ] && [ "${SOLR_SECURITY_MANAGER_ENABLED:-true}" == "true"  ]; then
+      # In this case we need to allow reads from the parent directory of the keystore
+      SOLR_SSL_OPTS+=" -Djavax.net.ssl.keyStoreParentPath=$SOLR_SSL_CLIENT_KEY_STORE/.."
+    fi
   else
     if [ -n "$SOLR_SSL_KEY_STORE" ]; then
       SOLR_SSL_OPTS+=" -Djavax.net.ssl.keyStore=$SOLR_SSL_KEY_STORE"
diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd
index 3a19be2e366..124ef286c14 100755
--- a/solr/bin/solr.cmd
+++ b/solr/bin/solr.cmd
@@ -84,11 +84,29 @@ IF NOT DEFINED SOLR_SSL_ENABLED (
   )
 )
 
+IF NOT DEFINED SOLR_SSL_RELOAD_ENABLED (
+  set "SOLR_SSL_RELOAD_ENABLED=true"
+)
+
+REM Enable java security manager by default (limiting filesystem access and other things)
+IF NOT DEFINED SOLR_SECURITY_MANAGER_ENABLED (
+  set SOLR_SECURITY_MANAGER_ENABLED=true
+)
+
 IF "%SOLR_SSL_ENABLED%"=="true" (
   set "SOLR_JETTY_CONFIG=--module=https --lib="%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*""
   set SOLR_URL_SCHEME=https
+  IF "%SOLR_SSL_RELOAD_ENABLED%"=="true" (
+    set "SOLR_JETTY_CONFIG=!SOLR_JETTY_CONFIG! --module=ssl-reload"
+    set "SOLR_SSL_OPTS=!SOLR_SSL_OPTS! -Dsolr.keyStoreReload.enabled=true"
+  )
   IF DEFINED SOLR_SSL_KEY_STORE (
     set "SOLR_SSL_OPTS=!SOLR_SSL_OPTS! -Dsolr.jetty.keystore=%SOLR_SSL_KEY_STORE%"
+    IF "%SOLR_SSL_RELOAD_ENABLED%"=="true" (
+      IF "%SOLR_SECURITY_MANAGER_ENABLED%"=="true" (
+        set "SOLR_SSL_OPTS=!SOLR_SSL_OPTS! -Dsolr.jetty.keystoreParentPath=%SOLR_SSL_KEY_STORE%/.."
+      )
+    )
   )
 
   IF DEFINED SOLR_SSL_KEY_STORE_TYPE (
@@ -122,6 +140,11 @@ IF "%SOLR_SSL_ENABLED%"=="true" (
     IF DEFINED SOLR_SSL_CLIENT_KEY_STORE_TYPE (
       set "SOLR_SSL_OPTS=!SOLR_SSL_OPTS! -Djavax.net.ssl.keyStoreType=%SOLR_SSL_CLIENT_KEY_STORE_TYPE%"
     )
+    IF "%SOLR_SSL_RELOAD_ENABLED%"=="true" (
+      IF "%SOLR_SECURITY_MANAGER_ENABLED%"=="true" (
+        set "SOLR_SSL_OPTS=!SOLR_SSL_OPTS! -Djavax.net.ssl.keyStoreParentPath=%SOLR_SSL_CLIENT_KEY_STORE_TYPE%/.."
+      )
+    )
   ) ELSE (
     IF DEFINED SOLR_SSL_KEY_STORE (
       set "SOLR_SSL_OPTS=!SOLR_SSL_OPTS! -Djavax.net.ssl.keyStore=%SOLR_SSL_KEY_STORE%"
@@ -1077,11 +1100,6 @@ IF "%ENABLE_REMOTE_JMX_OPTS%"=="true" (
   set REMOTE_JMX_OPTS=
 )
 
-REM Enable java security manager by default (limiting filesystem access and other things)
-IF NOT DEFINED SOLR_SECURITY_MANAGER_ENABLED (
-  set SOLR_SECURITY_MANAGER_ENABLED=true
-)
-
 IF "%SOLR_SECURITY_MANAGER_ENABLED%"=="true" (
   set SECURITY_MANAGER_OPTS=-Djava.security.manager ^
 -Djava.security.policy="%SOLR_SERVER_DIR%\etc\security.policy" ^
diff --git a/solr/bin/solr.in.sh b/solr/bin/solr.in.sh
index f6da91c2f3b..31cc131dfd3 100644
--- a/solr/bin/solr.in.sh
+++ b/solr/bin/solr.in.sh
@@ -179,6 +179,7 @@
 # Override Key/Trust Store types if necessary
 #SOLR_SSL_KEY_STORE_TYPE=PKCS12
 #SOLR_SSL_TRUST_STORE_TYPE=PKCS12
+#SOLR_SSL_RELOAD_ENABLED=true
 
 # Uncomment if you want to override previously defined SSL values for HTTP client
 # otherwise keep them commented and the above values will automatically be set for HTTP clients
diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
index 5eca1c272ed..80db530072e 100644
--- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
@@ -202,6 +202,7 @@ public class CreateTool extends ToolBase {
         new Http2SolrClient.Builder()
             .withIdleTimeout(30, TimeUnit.SECONDS)
             .withConnectionTimeout(15, TimeUnit.SECONDS)
+            .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
             .withOptionalBasicAuthCredentials(
                 cli.getOptionValue(SolrCLI.OPTION_CREDENTIALS.getLongOpt()));
     String zkHost = SolrCLI.getZkHost(cli);
diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
index f709c9defa2..26df3563675 100644
--- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
@@ -107,6 +107,7 @@ public class DeleteTool extends ToolBase {
         new Http2SolrClient.Builder()
             .withIdleTimeout(30, TimeUnit.SECONDS)
             .withConnectionTimeout(15, TimeUnit.SECONDS)
+            .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
             .withOptionalBasicAuthCredentials(cli.getOptionValue(("credentials")));
 
     String zkHost = SolrCLI.getZkHost(cli);
diff --git a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
index 7a6c21649f2..3995de32fd5 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -90,7 +91,9 @@ public class PostLogsTool extends ToolBase {
   public void runCommand(String baseUrl, String root, String credentials) throws IOException {
 
     Http2SolrClient.Builder builder =
-        new Http2SolrClient.Builder(baseUrl).withOptionalBasicAuthCredentials(credentials);
+        new Http2SolrClient.Builder(baseUrl)
+            .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
+            .withOptionalBasicAuthCredentials(credentials);
     try (SolrClient client = builder.build()) {
       int rec = 0;
       UpdateRequest request = new UpdateRequest();
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
index 14d9ab7f7c9..c30b972f9aa 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -416,6 +416,7 @@ public class SolrCLI implements CLIO {
     Http2SolrClient.Builder builder =
         new Http2SolrClient.Builder(solrUrl)
             .withMaxConnectionsPerHost(32)
+            .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
             .withOptionalBasicAuthCredentials(credentials);
 
     return builder.build();
diff --git a/solr/packaging/test/test_ssl.bats b/solr/packaging/test/test_ssl.bats
index 0af57e34420..7d02ebebe03 100644
--- a/solr/packaging/test/test_ssl.bats
+++ b/solr/packaging/test/test_ssl.bats
@@ -29,6 +29,7 @@ teardown() {
   solr stop -all >/dev/null 2>&1
 }
 
+
 @test "start solr with ssl" {
   # Create a keystore
   export ssl_dir="${BATS_TEST_TMPDIR}/ssl"
@@ -151,8 +152,8 @@ teardown() {
   export SOLR_HOST=localhost
 
   solr start -c
-  solr auth enable -type basicAuth -credentials name:password
   solr assert --started https://localhost:${SOLR_PORT}/solr --timeout 5000
+  solr auth enable -type basicAuth -credentials name:password
 
   run curl -u name:password --basic --cacert "$ssl_dir/solr-ssl.pem" "https://localhost:${SOLR_PORT}/solr/admin/collections?action=CREATE&collection.configName=_default&name=test&numShards=2&replicationFactor=1&router.name=compositeId&wt=json"
   assert_output --partial '"status":0'
@@ -209,13 +210,13 @@ teardown() {
 
   run solr start -c
 
+  solr assert --started https://localhost:${SOLR_PORT}/solr --timeout 5000
+
   export SOLR_SSL_KEY_STORE=
   export SOLR_SSL_KEY_STORE_PASSWORD=
   export SOLR_SSL_TRUST_STORE=
   export SOLR_SSL_TRUST_STORE_PASSWORD=
 
-  solr assert --started https://localhost:${SOLR_PORT}/solr --timeout 5000
-
   run solr create -c test -s 2
   assert_output --partial "Created collection 'test'"
 
@@ -494,3 +495,117 @@ teardown() {
   run solr api -verbose -get "https://localhost:${SOLR_PORT}/solr/test/select?q=*:*&rows=0"
   assert_output --regexp '(unable to find valid certification path to requested target|Server refused connection)'
 }
+
+@test "test keystore reload" {
+  # Create a keystore
+  export ssl_dir="${BATS_TEST_TMPDIR}/ssl"
+  mkdir -p "$ssl_dir"
+  (
+    cd "$ssl_dir"
+    rm -f cert1.keystore.p12 cert1.pem cert2.keystore.p12 cert2.pem
+    # cert and keystore 1
+    keytool -genkeypair -alias cert1 -keyalg RSA -keysize 2048 -keypass secret -storepass secret -validity 9999 -keystore cert1.keystore.p12 -storetype PKCS12 -ext SAN=DNS:localhost,IP:127.0.0.1 -dname "CN=localhost, OU=Organizational Unit, O=Organization, L=Location, ST=State, C=Country"
+    openssl pkcs12 -in cert1.keystore.p12 -out cert1.pem -passin pass:secret -passout pass:secret
+
+    # cert and keystore 2
+    keytool -genkeypair -alias cert2 -keyalg RSA -keysize 2048 -keypass secret -storepass secret -validity 9999 -keystore cert2.keystore.p12 -storetype PKCS12 -ext SAN=DNS:localhost,IP:127.0.0.1 -dname "CN=localhost, OU=Organizational Unit, O=Organization, L=Location, ST=State, C=Country"
+    openssl pkcs12 -in cert2.keystore.p12 -out cert2.pem -passin pass:secret -passout pass:secret
+
+    cp cert1.keystore.p12 server1.keystore.p12
+    cp cert1.keystore.p12 server2.keystore.p12
+  )
+
+  # Set ENV_VARs so that Solr uses this keystore
+  export SOLR_SSL_ENABLED=true
+  export SOLR_SSL_KEY_STORE_PASSWORD=secret
+  export SOLR_SSL_TRUST_STORE_PASSWORD=secret
+  export SOLR_SSL_NEED_CLIENT_AUTH=true
+  export SOLR_SSL_WANT_CLIENT_AUTH=false
+  export SOLR_HOST=localhost
+
+  # server1 will run on $SOLR_PORT and will use server1.keystore
+  export SOLR_SSL_KEY_STORE=$ssl_dir/server1.keystore.p12
+  export SOLR_SSL_TRUST_STORE=$ssl_dir/server1.keystore.p12
+  solr start -c -a "-Dsolr.jetty.sslContext.reload.scanInterval=1 -DsocketTimeout=5000"
+  solr assert --started https://localhost:${SOLR_PORT}/solr --timeout 5000
+
+  # server2 will run on $SOLR2_PORT and will use server2.keystore. Initially, this is the same as server1.keystore
+  export SOLR_SSL_KEY_STORE=$ssl_dir/server2.keystore.p12
+  export SOLR_SSL_TRUST_STORE=$ssl_dir/server2.keystore.p12
+  solr start -c -z localhost:${ZK_PORT} -p ${SOLR2_PORT} -a "-Dsolr.jetty.sslContext.reload.scanInterval=1 -DsocketTimeout=5000"
+  solr assert --started https://localhost:${SOLR2_PORT}/solr --timeout 5000
+
+  # "test" collection is two shards, meaning there must be communication between shards for queries (handled by http shard handler factory)
+  run solr create -c test -s 2
+  assert_output --partial "Created collection 'test'"
+
+  # "test-single-shard" is one shard and one replica, this means that one of the nodes will have to forward requests to the other
+  run solr create -c test-single-shard -s 1
+  assert_output --partial "Created collection 'test-single-shard'"
+
+  run solr api -get "https://localhost:${SOLR_PORT}/solr/test/select?q=*:*"
+  assert_output --partial '"numFound":0'
+  run solr api -get "https://localhost:${SOLR2_PORT}/solr/test/select?q=*:*"
+  assert_output --partial '"numFound":0'
+
+  run solr api -get "https://localhost:${SOLR_PORT}/solr/test-single-shard/select?q=*:*"
+  assert_output --partial '"numFound":0'
+  run solr api -get "https://localhost:${SOLR2_PORT}/solr/test-single-shard/select?q=*:*"
+  assert_output --partial '"numFound":0'
+
+  run ! curl "https://localhost:${SOLR_PORT}/solr/test/select?q=*:*"
+  run ! curl "https://localhost:${SOLR2_PORT}/solr/test/select?q=*:*"
+
+  run ! curl "https://localhost:${SOLR_PORT}/solr/test-single-shard/select?q=*:*"
+  run ! curl "https://localhost:${SOLR2_PORT}/solr/test-single-shard/select?q=*:*"
+
+  export SOLR_SSL_KEY_STORE=$ssl_dir/cert2.keystore.p12
+  export SOLR_SSL_KEY_STORE_PASSWORD=secret
+  export SOLR_SSL_TRUST_STORE=$ssl_dir/cert2.keystore.p12
+  export SOLR_SSL_TRUST_STORE_PASSWORD=secret
+
+  run ! solr api -get "https://localhost:${SOLR_PORT}/solr/test/select?q=*:*"
+
+  (
+    cd "$ssl_dir"
+    # Replace server1 keystore with client's
+    cp cert2.keystore.p12 server1.keystore.p12
+  )
+  # Give some time for the server reload
+  sleep 6
+
+  run solr healthcheck -solrUrl https://localhost:${SOLR_PORT}
+
+  # Server 2 still uses the cert1, so this request should fail
+  run ! solr api -get "https://localhost:${SOLR2_PORT}/solr/test/select?q=query2"
+
+  run ! solr healthcheck -solrUrl https://localhost:${SOLR2_PORT}
+
+  (
+    cd "$ssl_dir"
+    # Replace server2 keystore with client's
+    cp cert2.keystore.p12 server2.keystore.p12
+  )
+  # Give some time for the server reload
+  sleep 6
+
+  run solr healthcheck -solrUrl https://localhost:${SOLR_PORT}
+  run solr healthcheck -solrUrl https://localhost:${SOLR2_PORT}
+
+  run solr api -get "https://localhost:${SOLR_PORT}/solr/test/select?q=query3"
+  assert_output --partial '"numFound":0'
+
+  run solr api -get "https://localhost:${SOLR2_PORT}/solr/test/select?q=query3"
+  assert_output --partial '"numFound":0'
+
+  run solr api -get "https://localhost:${SOLR_PORT}/solr/test-single-shard/select?q=query4"
+  assert_output --partial '"numFound":0'
+
+  run solr api -get "https://localhost:${SOLR2_PORT}/solr/test-single-shard/select?q=query4"
+  assert_output --partial '"numFound":0'
+
+  run solr post -url https://localhost:${SOLR_PORT}/solr/test/update -commit ${SOLR_TIP}/example/exampledocs/books.csv
+
+  run solr api -get "https://localhost:${SOLR_PORT}/solr/test/select?q=*:*"
+  assert_output --partial '"numFound":10'
+}
\ No newline at end of file
diff --git a/solr/server/etc/jetty-ssl-context-reload.xml b/solr/server/etc/jetty-ssl-context-reload.xml
new file mode 100644
index 00000000000..827d80c3529
--- /dev/null
+++ b/solr/server/etc/jetty-ssl-context-reload.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_10_0.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+    <Call name="addBean">
+        <Arg>
+            <New id="keyStoreScanner" class="org.eclipse.jetty.util.ssl.KeyStoreScanner">
+                <Arg><Ref refid="sslContextFactory"/></Arg>
+                <Set name="scanInterval"><Property name="solr.jetty.sslContext.reload.scanInterval" default="30"/></Set>
+            </New>
+        </Arg>
+    </Call>
+</Configure>
diff --git a/solr/server/etc/security.policy b/solr/server/etc/security.policy
index aec2e2ddcfe..1dd9db3aef8 100644
--- a/solr/server/etc/security.policy
+++ b/solr/server/etc/security.policy
@@ -193,6 +193,9 @@ grant {
 
   permission java.io.FilePermission "${javax.net.ssl.trustStore}", "read,readlink";
 
+  permission java.io.FilePermission "${solr.jetty.keystoreParentPath}", "read,readlink";
+  permission java.io.FilePermission "${javax.net.ssl.keyStoreParentPath}", "read,readlink";
+
   permission java.io.FilePermission "${solr.install.dir}", "read,write,delete,readlink";
   permission java.io.FilePermission "${solr.install.dir}${/}-", "read,write,delete,readlink";
   permission java.io.FilePermission "${solr.install.symDir}", "read,write,delete,readlink";
diff --git a/solr/server/modules/ssl-reload.mod b/solr/server/modules/ssl-reload.mod
new file mode 100644
index 00000000000..60e52537733
--- /dev/null
+++ b/solr/server/modules/ssl-reload.mod
@@ -0,0 +1,12 @@
+[description]
+Enables the KeyStore to be reloaded when the KeyStore file changes.
+
+[tags]
+connector
+ssl
+
+[depend]
+ssl
+
+[xml]
+etc/jetty-ssl-context-reload.xml
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/enabling-ssl.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/enabling-ssl.adoc
index 685acdb5da4..38ca9cd9e2f 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/enabling-ssl.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/enabling-ssl.adoc
@@ -323,6 +323,23 @@ C:\> bin\solr.cmd -cloud -s cloud\node1 -z server1:2181,server2:2181,server3:218
 ====
 --
 
+== Automatically reloading KeyStore/TrustStore
+=== Solr Server
+Solr can automatically reload KeyStore/TrustStore when certificates are updated without restarting. This is enabled by default
+when using SSL, but can be disabled by setting the environment variable `SOLR_SSL_RELOAD_ENABLED` to `false`. By
+default, Solr will check for updates in the KeyStore every 30 seconds, but this interval can be updated by passing the
+system property `solr.jetty.sslContext.reload.scanInterval` with the new interval in seconds on startup.
+Note that the truststore file is not actively monitored, so if you need to apply changes to the truststore, you need
+to update it and after that touch the keystore to trigger a reload.
+
+=== SolrJ client
+Http2SolrClient builder has a method `withKeyStoreReloadInterval(long interval, TimeUnit unit)` to initialize a scanner
+that will watch and update the keystore and truststore for changes. If you are using CloudHttp2SolrClient, you can use
+the `withInternalClientBuilder(Http2SolrClient.Builder internalClientBuilder)` to configure the internal http client
+with a keystore reload interval. The minimum reload interval is 1 second. If not set (or set to 0 or a negative value),
+the keystore/truststore won't be updated in the client.
+
+
 == Example Client Actions
 
 [IMPORTANT]
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
index 0f060000d30..00e94715cfd 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
@@ -105,6 +105,7 @@ import org.eclipse.jetty.io.ClientConnector;
 import org.eclipse.jetty.util.BlockingArrayQueue;
 import org.eclipse.jetty.util.Fields;
 import org.eclipse.jetty.util.HttpCookieStore;
+import org.eclipse.jetty.util.ssl.KeyStoreScanner;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -157,6 +158,8 @@ public class Http2SolrClient extends SolrClient {
   final String basicAuthAuthorizationStr;
   private AuthenticationStoreHolder authenticationStore;
 
+  private KeyStoreScanner scanner;
+
   protected Http2SolrClient(String serverBaseUrl, Builder builder) {
     if (serverBaseUrl != null) {
       if (!serverBaseUrl.equals("/") && serverBaseUrl.endsWith("/")) {
@@ -237,6 +240,30 @@ public class Http2SolrClient extends SolrClient {
       sslContextFactory = builder.sslConfig.createClientContextFactory();
     }
 
+    if (sslContextFactory != null
+        && sslContextFactory.getKeyStoreResource() != null
+        && builder.keyStoreReloadIntervalSecs != null
+        && builder.keyStoreReloadIntervalSecs > 0) {
+      scanner = new KeyStoreScanner(sslContextFactory);
+      try {
+        scanner.setScanInterval(
+            (int) Math.min(builder.keyStoreReloadIntervalSecs, Integer.MAX_VALUE));
+        scanner.start();
+        if (log.isDebugEnabled()) {
+          log.debug("Key Store Scanner started");
+        }
+      } catch (Exception e) {
+        RuntimeException startException =
+            new RuntimeException("Unable to start key store scanner", e);
+        try {
+          scanner.stop();
+        } catch (Exception stopException) {
+          startException.addSuppressed(stopException);
+        }
+        throw startException;
+      }
+    }
+
     ClientConnector clientConnector = new ClientConnector();
     clientConnector.setReuseAddress(true);
     clientConnector.setSslContextFactory(sslContextFactory);
@@ -325,6 +352,14 @@ public class Http2SolrClient extends SolrClient {
       if (closeClient) {
         httpClient.stop();
         httpClient.destroy();
+
+        if (scanner != null) {
+          scanner.stop();
+          if (log.isDebugEnabled()) {
+            log.debug("Key Store Scanner stopped");
+          }
+          scanner = null;
+        }
       }
     } catch (Exception e) {
       throw new RuntimeException("Exception on closing client", e);
@@ -333,7 +368,6 @@ public class Http2SolrClient extends SolrClient {
         ExecutorUtil.shutdownAndAwaitTermination(executor);
       }
     }
-
     assert ObjectReleaseTracker.release(this);
   }
 
@@ -1042,6 +1076,7 @@ public class Http2SolrClient extends SolrClient {
     private int proxyPort;
     private boolean proxyIsSocks4;
     private boolean proxyIsSecure;
+    private Long keyStoreReloadIntervalSecs;
 
     public Builder() {}
 
@@ -1057,6 +1092,17 @@ public class Http2SolrClient extends SolrClient {
         connectionTimeoutMillis = (long) HttpClientUtil.DEFAULT_CONNECT_TIMEOUT;
       }
 
+      if (keyStoreReloadIntervalSecs != null
+          && keyStoreReloadIntervalSecs > 0
+          && this.httpClient != null) {
+        log.warn("keyStoreReloadIntervalSecs can't be set when using external httpClient");
+        keyStoreReloadIntervalSecs = null;
+      } else if (keyStoreReloadIntervalSecs == null
+          && this.httpClient == null
+          && Boolean.getBoolean("solr.keyStoreReload.enabled")) {
+        keyStoreReloadIntervalSecs = Long.getLong("solr.jetty.sslContext.reload.scanInterval", 30);
+      }
+
       Http2SolrClient client = new Http2SolrClient(baseSolrUrl, this);
       try {
         httpClientBuilderSetup(client);
@@ -1208,6 +1254,23 @@ public class Http2SolrClient extends SolrClient {
       return this;
     }
 
+    /**
+     * Set the scanning interval to check for updates in the Key Store used by this client. If the
+     * interval is unset, 0 or less, then the Key Store Scanner is not created, and the client will
+     * not attempt to update key stores. The minimum value between checks is 1 second.
+     *
+     * @param interval Interval between checks
+     * @param unit The unit for the interval
+     * @return This builder
+     */
+    public Builder withKeyStoreReloadInterval(long interval, TimeUnit unit) {
+      this.keyStoreReloadIntervalSecs = unit.toSeconds(interval);
+      if (this.keyStoreReloadIntervalSecs == 0 && interval > 0) {
+        this.keyStoreReloadIntervalSecs = 1L;
+      }
+      return this;
+    }
+
     /**
      * @deprecated Please use {@link #withIdleTimeout(long, TimeUnit)}
      */