This is an automated email from the ASF dual-hosted git repository.
deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new 709d06bce95 HIVE-29636: Add SSL keystore auto-reloading for
HiveServer2 WebUI (#6514)
709d06bce95 is described below
commit 709d06bce95df7dc66c63f90ce99aadf8f24f489
Author: magnuma3 <[email protected]>
AuthorDate: Tue Jun 9 04:24:51 2026 +0900
HIVE-29636: Add SSL keystore auto-reloading for HiveServer2 WebUI (#6514)
---
.../java/org/apache/hadoop/hive/conf/HiveConf.java | 4 +
.../src/java/org/apache/hive/http/HttpServer.java | 45 ++++++
.../test/org/apache/hive/http/TestHttpServer.java | 174 +++++++++++++++++++++
3 files changed, 223 insertions(+)
diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
index 31b5e32c2dd..662ece1c907 100644
--- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -3856,6 +3856,10 @@ public static enum ConfVars {
"SSL certificate keystore location for HiveServer2 WebUI."),
HIVE_SERVER2_WEBUI_SSL_KEYSTORE_PASSWORD("hive.server2.webui.keystore.password",
"",
"SSL certificate keystore password for HiveServer2 WebUI."),
+
HIVE_SERVER2_WEBUI_SSL_KEYSTORE_RELOAD_INTERVAL("hive.server2.webui.keystore.reload.interval",
"0",
+ new TimeValidator(TimeUnit.MILLISECONDS),
+ "Interval at which HiveServer2 WebUI checks the SSL keystore file for
changes; " +
+ "set to 0 to disable auto-reload. The default is 0."),
HIVE_SERVER2_WEBUI_SSL_KEYSTORE_TYPE("hive.server2.webui.keystore.type",
"",
"SSL certificate keystore type for HiveServer2 WebUI."),
HIVE_SERVER2_WEBUI_SSL_INCLUDE_CIPHERSUITES("hive.server2.webui.include.ciphersuites",
"",
diff --git a/common/src/java/org/apache/hive/http/HttpServer.java
b/common/src/java/org/apache/hive/http/HttpServer.java
index dd9e66f92b6..7169c0929eb 100644
--- a/common/src/java/org/apache/hive/http/HttpServer.java
+++ b/common/src/java/org/apache/hive/http/HttpServer.java
@@ -35,6 +35,8 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.Timer;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -51,6 +53,7 @@
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
@@ -66,6 +69,7 @@
import org.apache.hadoop.security.authorize.AccessControlList;
import org.apache.hadoop.hive.common.classification.InterfaceAudience;
import org.apache.hadoop.security.http.CrossOriginFilter;
+import org.apache.hadoop.security.ssl.FileMonitoringTimerTask;
import org.apache.hive.http.security.PamAuthenticator;
import org.apache.hive.http.security.PamConstraint;
import org.apache.hive.http.security.PamConstraintMapping;
@@ -140,6 +144,8 @@ public class HttpServer {
private Server webServer;
private QueuedThreadPool threadPool;
private PortHandlerWrapper portHandlerWrapper;
+ @VisibleForTesting
+ Timer keystoreChangeMonitor;
/**
* Create a status server on the given port.
@@ -360,6 +366,10 @@ public void start() throws Exception {
}
public void stop() throws Exception {
+ if (this.keystoreChangeMonitor != null) {
+ this.keystoreChangeMonitor.cancel();
+ this.keystoreChangeMonitor = null;
+ }
webServer.stop();
}
@@ -695,6 +705,11 @@ ServerConnector createAndAddChannelConnector(int
queueSize, Builder b) {
new String[excludedSSLProtocols.size()]));
sslContextFactory.setKeyStorePassword(b.keyStorePassword);
connector = new ServerConnector(webServer, sslContextFactory, http);
+
+ long reloadInterval =
b.conf.getTimeVar(ConfVars.HIVE_SERVER2_WEBUI_SSL_KEYSTORE_RELOAD_INTERVAL,
TimeUnit.MILLISECONDS);
+ if (reloadInterval > 0) {
+ this.keystoreChangeMonitor =
createKeystoreChangeMonitor(reloadInterval, b.keyStorePath, sslContextFactory);
+ }
}
connector.setAcceptQueueSize(queueSize);
@@ -706,6 +721,36 @@ ServerConnector createAndAddChannelConnector(int
queueSize, Builder b) {
return connector;
}
+ @VisibleForTesting
+ void setKeystoreChangeMonitor(Timer monitor) {
+ keystoreChangeMonitor = monitor;
+ }
+
+ @VisibleForTesting
+ Timer createKeystoreChangeMonitor(long reloadInterval, String keyStorePath,
+ SslContextFactory sslContextFactory) {
+ LOG.info("Starting SSL Certificates Store Monitor. reload interval: {}ms,
keyStorePath: {}", reloadInterval, keyStorePath);
+ Timer timer = new Timer("SSL Certificates Store Monitor", true);
+ //
+ // The Jetty SSLContextFactory provides a 'reload' method which will
reload both
+ // truststore and keystore certificates.
+ //
+ timer.schedule(new FileMonitoringTimerTask(
+ Paths.get(keyStorePath),
+ path -> {
+ LOG.info("Reloading certificates from store keystore {}",
keyStorePath);
+ try {
+ sslContextFactory.reload(factory -> { });
+ } catch (Exception ex) {
+ LOG.error("Failed to reload SSL keystore certificates", ex);
+ }
+ },null),
+ reloadInterval,
+ reloadInterval
+ );
+ return timer;
+ }
+
/**
* Secure the web server with PAM.
*/
diff --git a/common/src/test/org/apache/hive/http/TestHttpServer.java
b/common/src/test/org/apache/hive/http/TestHttpServer.java
new file mode 100644
index 00000000000..75e27f1e642
--- /dev/null
+++ b/common/src/test/org/apache/hive/http/TestHttpServer.java
@@ -0,0 +1,174 @@
+/*
+ * 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.hive.http;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.Timer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
+
+/**
+ * Tests for the SSL keystore auto-reload feature wired in via
+ * {@code HttpServer#makeConfigurationChangeMonitor} and the surrounding
+ * {@code configurationChangeMonitor} field. See HiveConf
+ * {@code hive.server2.webui.keystore.reload.interval}.
+ */
+public class TestHttpServer {
+
+ private Path keystore;
+ private Timer timer;
+
+ @Before
+ public void setUp() throws Exception {
+ keystore = Files.createTempFile("test-keystore-", ".jks");
+ Files.write(keystore, "initial-content".getBytes());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (timer != null) {
+ timer.cancel();
+ }
+ if (keystore != null) {
+ Files.deleteIfExists(keystore);
+ }
+ }
+
+ /**
+ * When the watched keystore file is modified, the scheduled
+ * {@code FileMonitoringTimerTask} must invoke
+ * {@code SslContextFactory#reload}.
+ */
+ @Test(timeout = 10_000)
+ public void testMonitorReloadsSslContextOnKeystoreModification() throws
Exception {
+ SslContextFactory sslContextFactory = mock(SslContextFactory.class);
+ CountDownLatch reloadCalled = new CountDownLatch(1);
+ doAnswer(invocation -> {
+ reloadCalled.countDown();
+ return null;
+ }).when(sslContextFactory).reload(any());
+
+ timer = invokeMakeMonitor(100L, keystore.toString(), sslContextFactory);
+
+ // Bump mtime to guarantee a detected change (FileMonitoringTimerTask
compares mtimes).
+ Files.setLastModifiedTime(keystore,
FileTime.fromMillis(System.currentTimeMillis() + 5_000));
+
+ assertTrue("SslContextFactory#reload was not called within 5s of keystore
mtime change",
+ reloadCalled.await(5, TimeUnit.SECONDS));
+ verify(sslContextFactory, atLeastOnce()).reload(any());
+ }
+
+ /**
+ * Reload failures must be swallowed so a transient bad keystore can't take
HS2 down;
+ * the next mtime change should still trigger another reload attempt.
+ */
+ @Test(timeout = 10_000)
+ public void testMonitorSurvivesReloadException() throws Exception {
+ SslContextFactory sslContextFactory = mock(SslContextFactory.class);
+ CountDownLatch reloadCalled = new CountDownLatch(2);
+ doAnswer(invocation -> {
+ reloadCalled.countDown();
+ throw new RuntimeException("simulated keystore reload failure");
+ }).when(sslContextFactory).reload(any());
+
+ timer = invokeMakeMonitor(100L, keystore.toString(), sslContextFactory);
+
+ Files.setLastModifiedTime(keystore,
FileTime.fromMillis(System.currentTimeMillis() + 5_000));
+ Thread.sleep(300);
+ Files.setLastModifiedTime(keystore,
FileTime.fromMillis(System.currentTimeMillis() + 10_000));
+
+ assertTrue("Monitor should keep firing reload attempts even after
exceptions",
+ reloadCalled.await(5, TimeUnit.SECONDS));
+ }
+
+ /**
+ * {@code stop()} must cancel the monitor Timer when one was installed,
+ * so the daemon thread does not outlive HS2.
+ */
+ @Test
+ public void testStopCancelsConfigurationChangeMonitor() throws Exception {
+ HttpServer server = mock(HttpServer.class,
withSettings().defaultAnswer(CALLS_REAL_METHODS));
+
+ // Track whether cancel() was invoked on the installed timer.
+ boolean[] cancelled = {false};
+ Timer installed = new Timer("test-monitor", true) {
+ @Override
+ public void cancel() {
+ cancelled[0] = true;
+ super.cancel();
+ }
+ };
+ server.setKeystoreChangeMonitor(installed);
+
+ // stop() also calls webServer.stop(); webServer is null on a mock, so we
expect
+ // a NullPointerException after the cancel path runs.
+ try {
+ server.stop();
+ } catch (NullPointerException expected) {
+ // intentionally ignored — we only assert the monitor was cancelled
+ }
+ assertTrue("Timer#cancel should have been invoked from stop()",
cancelled[0]);
+ }
+
+ /**
+ * No monitor installed → stop() must not blow up trying to cancel a missing
Timer.
+ * (Mockito skips field initializers, so we re-establish the production
default
+ * {@code Optional.empty()} on the mock before exercising stop().)
+ */
+ @Test
+ public void testStopWithoutMonitorDoesNotThrowFromCancelPath() throws
Exception {
+ HttpServer server = mock(HttpServer.class,
withSettings().defaultAnswer(CALLS_REAL_METHODS));
+ server.setKeystoreChangeMonitor(null);
+ assertNull("keystoreChangeMonitor should be empty for this case",
server.keystoreChangeMonitor);
+
+ try {
+ server.stop();
+ } catch (NullPointerException expectedFromWebServerStop) {
+ // ok — the monitor branch must not have thrown before reaching
webServer.stop()
+ }
+ }
+
+ // ---- reflection helpers ------------------------------------------------
+
+ private static Timer invokeMakeMonitor(long intervalMs, String keystorePath,
+ SslContextFactory sslContextFactory)
throws Exception {
+ HttpServer server = mock(HttpServer.class,
withSettings().defaultAnswer(CALLS_REAL_METHODS));
+ return server.createKeystoreChangeMonitor(intervalMs, keystorePath,
sslContextFactory);
+ }
+}