Skip to content

Commit 709d06b

Browse files
authored
HIVE-29636: Add SSL keystore auto-reloading for HiveServer2 WebUI (#6514)
1 parent e8299d2 commit 709d06b

3 files changed

Lines changed: 223 additions & 0 deletions

File tree

common/src/java/org/apache/hadoop/hive/conf/HiveConf.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3856,6 +3856,10 @@ public static enum ConfVars {
38563856
"SSL certificate keystore location for HiveServer2 WebUI."),
38573857
HIVE_SERVER2_WEBUI_SSL_KEYSTORE_PASSWORD("hive.server2.webui.keystore.password", "",
38583858
"SSL certificate keystore password for HiveServer2 WebUI."),
3859+
HIVE_SERVER2_WEBUI_SSL_KEYSTORE_RELOAD_INTERVAL("hive.server2.webui.keystore.reload.interval", "0",
3860+
new TimeValidator(TimeUnit.MILLISECONDS),
3861+
"Interval at which HiveServer2 WebUI checks the SSL keystore file for changes; " +
3862+
"set to 0 to disable auto-reload. The default is 0."),
38593863
HIVE_SERVER2_WEBUI_SSL_KEYSTORE_TYPE("hive.server2.webui.keystore.type", "",
38603864
"SSL certificate keystore type for HiveServer2 WebUI."),
38613865
HIVE_SERVER2_WEBUI_SSL_INCLUDE_CIPHERSUITES("hive.server2.webui.include.ciphersuites", "",

common/src/java/org/apache/hive/http/HttpServer.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.Map;
3636
import java.util.Optional;
3737
import java.util.Set;
38+
import java.util.Timer;
39+
import java.util.concurrent.TimeUnit;
3840
import java.util.regex.Matcher;
3941
import java.util.regex.Pattern;
4042

@@ -51,6 +53,7 @@
5153
import javax.servlet.http.HttpServletRequestWrapper;
5254
import javax.servlet.http.HttpServletResponse;
5355

56+
import com.google.common.annotations.VisibleForTesting;
5457
import com.google.common.base.Preconditions;
5558

5659
import org.apache.commons.lang3.StringUtils;
@@ -66,6 +69,7 @@
6669
import org.apache.hadoop.security.authorize.AccessControlList;
6770
import org.apache.hadoop.hive.common.classification.InterfaceAudience;
6871
import org.apache.hadoop.security.http.CrossOriginFilter;
72+
import org.apache.hadoop.security.ssl.FileMonitoringTimerTask;
6973
import org.apache.hive.http.security.PamAuthenticator;
7074
import org.apache.hive.http.security.PamConstraint;
7175
import org.apache.hive.http.security.PamConstraintMapping;
@@ -140,6 +144,8 @@ public class HttpServer {
140144
private Server webServer;
141145
private QueuedThreadPool threadPool;
142146
private PortHandlerWrapper portHandlerWrapper;
147+
@VisibleForTesting
148+
Timer keystoreChangeMonitor;
143149

144150
/**
145151
* Create a status server on the given port.
@@ -360,6 +366,10 @@ public void start() throws Exception {
360366
}
361367

362368
public void stop() throws Exception {
369+
if (this.keystoreChangeMonitor != null) {
370+
this.keystoreChangeMonitor.cancel();
371+
this.keystoreChangeMonitor = null;
372+
}
363373
webServer.stop();
364374
}
365375

@@ -695,6 +705,11 @@ ServerConnector createAndAddChannelConnector(int queueSize, Builder b) {
695705
new String[excludedSSLProtocols.size()]));
696706
sslContextFactory.setKeyStorePassword(b.keyStorePassword);
697707
connector = new ServerConnector(webServer, sslContextFactory, http);
708+
709+
long reloadInterval = b.conf.getTimeVar(ConfVars.HIVE_SERVER2_WEBUI_SSL_KEYSTORE_RELOAD_INTERVAL, TimeUnit.MILLISECONDS);
710+
if (reloadInterval > 0) {
711+
this.keystoreChangeMonitor = createKeystoreChangeMonitor(reloadInterval, b.keyStorePath, sslContextFactory);
712+
}
698713
}
699714

700715
connector.setAcceptQueueSize(queueSize);
@@ -706,6 +721,36 @@ ServerConnector createAndAddChannelConnector(int queueSize, Builder b) {
706721
return connector;
707722
}
708723

724+
@VisibleForTesting
725+
void setKeystoreChangeMonitor(Timer monitor) {
726+
keystoreChangeMonitor = monitor;
727+
}
728+
729+
@VisibleForTesting
730+
Timer createKeystoreChangeMonitor(long reloadInterval, String keyStorePath,
731+
SslContextFactory sslContextFactory) {
732+
LOG.info("Starting SSL Certificates Store Monitor. reload interval: {}ms, keyStorePath: {}", reloadInterval, keyStorePath);
733+
Timer timer = new Timer("SSL Certificates Store Monitor", true);
734+
//
735+
// The Jetty SSLContextFactory provides a 'reload' method which will reload both
736+
// truststore and keystore certificates.
737+
//
738+
timer.schedule(new FileMonitoringTimerTask(
739+
Paths.get(keyStorePath),
740+
path -> {
741+
LOG.info("Reloading certificates from store keystore {}", keyStorePath);
742+
try {
743+
sslContextFactory.reload(factory -> { });
744+
} catch (Exception ex) {
745+
LOG.error("Failed to reload SSL keystore certificates", ex);
746+
}
747+
},null),
748+
reloadInterval,
749+
reloadInterval
750+
);
751+
return timer;
752+
}
753+
709754
/**
710755
* Secure the web server with PAM.
711756
*/
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hive.http;
19+
20+
import org.apache.hadoop.hive.conf.HiveConf;
21+
import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
22+
import org.eclipse.jetty.util.ssl.SslContextFactory;
23+
import org.junit.After;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.attribute.FileTime;
30+
import java.util.Timer;
31+
import java.util.concurrent.CountDownLatch;
32+
import java.util.concurrent.TimeUnit;
33+
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertFalse;
36+
import static org.junit.Assert.assertNull;
37+
import static org.junit.Assert.assertTrue;
38+
import static org.mockito.ArgumentMatchers.any;
39+
import static org.mockito.Mockito.CALLS_REAL_METHODS;
40+
import static org.mockito.Mockito.atLeastOnce;
41+
import static org.mockito.Mockito.doAnswer;
42+
import static org.mockito.Mockito.mock;
43+
import static org.mockito.Mockito.verify;
44+
import static org.mockito.Mockito.withSettings;
45+
46+
/**
47+
* Tests for the SSL keystore auto-reload feature wired in via
48+
* {@code HttpServer#makeConfigurationChangeMonitor} and the surrounding
49+
* {@code configurationChangeMonitor} field. See HiveConf
50+
* {@code hive.server2.webui.keystore.reload.interval}.
51+
*/
52+
public class TestHttpServer {
53+
54+
private Path keystore;
55+
private Timer timer;
56+
57+
@Before
58+
public void setUp() throws Exception {
59+
keystore = Files.createTempFile("test-keystore-", ".jks");
60+
Files.write(keystore, "initial-content".getBytes());
61+
}
62+
63+
@After
64+
public void tearDown() throws Exception {
65+
if (timer != null) {
66+
timer.cancel();
67+
}
68+
if (keystore != null) {
69+
Files.deleteIfExists(keystore);
70+
}
71+
}
72+
73+
/**
74+
* When the watched keystore file is modified, the scheduled
75+
* {@code FileMonitoringTimerTask} must invoke
76+
* {@code SslContextFactory#reload}.
77+
*/
78+
@Test(timeout = 10_000)
79+
public void testMonitorReloadsSslContextOnKeystoreModification() throws Exception {
80+
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
81+
CountDownLatch reloadCalled = new CountDownLatch(1);
82+
doAnswer(invocation -> {
83+
reloadCalled.countDown();
84+
return null;
85+
}).when(sslContextFactory).reload(any());
86+
87+
timer = invokeMakeMonitor(100L, keystore.toString(), sslContextFactory);
88+
89+
// Bump mtime to guarantee a detected change (FileMonitoringTimerTask compares mtimes).
90+
Files.setLastModifiedTime(keystore, FileTime.fromMillis(System.currentTimeMillis() + 5_000));
91+
92+
assertTrue("SslContextFactory#reload was not called within 5s of keystore mtime change",
93+
reloadCalled.await(5, TimeUnit.SECONDS));
94+
verify(sslContextFactory, atLeastOnce()).reload(any());
95+
}
96+
97+
/**
98+
* Reload failures must be swallowed so a transient bad keystore can't take HS2 down;
99+
* the next mtime change should still trigger another reload attempt.
100+
*/
101+
@Test(timeout = 10_000)
102+
public void testMonitorSurvivesReloadException() throws Exception {
103+
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
104+
CountDownLatch reloadCalled = new CountDownLatch(2);
105+
doAnswer(invocation -> {
106+
reloadCalled.countDown();
107+
throw new RuntimeException("simulated keystore reload failure");
108+
}).when(sslContextFactory).reload(any());
109+
110+
timer = invokeMakeMonitor(100L, keystore.toString(), sslContextFactory);
111+
112+
Files.setLastModifiedTime(keystore, FileTime.fromMillis(System.currentTimeMillis() + 5_000));
113+
Thread.sleep(300);
114+
Files.setLastModifiedTime(keystore, FileTime.fromMillis(System.currentTimeMillis() + 10_000));
115+
116+
assertTrue("Monitor should keep firing reload attempts even after exceptions",
117+
reloadCalled.await(5, TimeUnit.SECONDS));
118+
}
119+
120+
/**
121+
* {@code stop()} must cancel the monitor Timer when one was installed,
122+
* so the daemon thread does not outlive HS2.
123+
*/
124+
@Test
125+
public void testStopCancelsConfigurationChangeMonitor() throws Exception {
126+
HttpServer server = mock(HttpServer.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
127+
128+
// Track whether cancel() was invoked on the installed timer.
129+
boolean[] cancelled = {false};
130+
Timer installed = new Timer("test-monitor", true) {
131+
@Override
132+
public void cancel() {
133+
cancelled[0] = true;
134+
super.cancel();
135+
}
136+
};
137+
server.setKeystoreChangeMonitor(installed);
138+
139+
// stop() also calls webServer.stop(); webServer is null on a mock, so we expect
140+
// a NullPointerException after the cancel path runs.
141+
try {
142+
server.stop();
143+
} catch (NullPointerException expected) {
144+
// intentionally ignored — we only assert the monitor was cancelled
145+
}
146+
assertTrue("Timer#cancel should have been invoked from stop()", cancelled[0]);
147+
}
148+
149+
/**
150+
* No monitor installed → stop() must not blow up trying to cancel a missing Timer.
151+
* (Mockito skips field initializers, so we re-establish the production default
152+
* {@code Optional.empty()} on the mock before exercising stop().)
153+
*/
154+
@Test
155+
public void testStopWithoutMonitorDoesNotThrowFromCancelPath() throws Exception {
156+
HttpServer server = mock(HttpServer.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
157+
server.setKeystoreChangeMonitor(null);
158+
assertNull("keystoreChangeMonitor should be empty for this case", server.keystoreChangeMonitor);
159+
160+
try {
161+
server.stop();
162+
} catch (NullPointerException expectedFromWebServerStop) {
163+
// ok — the monitor branch must not have thrown before reaching webServer.stop()
164+
}
165+
}
166+
167+
// ---- reflection helpers ------------------------------------------------
168+
169+
private static Timer invokeMakeMonitor(long intervalMs, String keystorePath,
170+
SslContextFactory sslContextFactory) throws Exception {
171+
HttpServer server = mock(HttpServer.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
172+
return server.createKeystoreChangeMonitor(intervalMs, keystorePath, sslContextFactory);
173+
}
174+
}

0 commit comments

Comments
 (0)