Skip to content

Commit 6ce6744

Browse files
committed
HBASE-28911: Automatic SSL keystore reloading for HttpServer
implement the same as HADOOP-16524
1 parent 402aa92 commit 6ce6744

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.HashMap;
3636
import java.util.List;
3737
import java.util.Map;
38+
import java.util.Optional;
39+
import java.util.Timer;
3840
import java.util.stream.Collectors;
3941
import javax.servlet.Filter;
4042
import javax.servlet.FilterChain;
@@ -62,6 +64,8 @@
6264
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
6365
import org.apache.hadoop.security.authorize.AccessControlList;
6466
import org.apache.hadoop.security.authorize.ProxyUsers;
67+
import org.apache.hadoop.security.ssl.FileBasedKeyStoresFactory;
68+
import org.apache.hadoop.security.ssl.FileMonitoringTimerTask;
6569
import org.apache.hadoop.util.Shell;
6670
import org.apache.hadoop.util.StringUtils;
6771
import org.apache.yetus.audience.InterfaceAudience;
@@ -179,6 +183,7 @@ public class HttpServer implements FilterContainer {
179183
.build();
180184

181185
private final AccessControlList adminsAcl;
186+
private Optional<Timer> configurationChangeMonitor = Optional.empty();
182187

183188
protected final Server webServer;
184189
protected String appDir;
@@ -466,6 +471,15 @@ public HttpServer build() throws IOException {
466471
LOG.debug("Excluded SSL Cipher List:" + excludeCiphers);
467472
}
468473

474+
long storesReloadInterval =
475+
conf.getLong(FileBasedKeyStoresFactory.SSL_STORES_RELOAD_INTERVAL_TPL_KEY,
476+
FileBasedKeyStoresFactory.DEFAULT_SSL_STORES_RELOAD_INTERVAL);
477+
478+
if (storesReloadInterval > 0 && (keyStore != null || trustStore != null)) {
479+
server.configurationChangeMonitor =
480+
Optional.of(this.makeConfigurationChangeMonitor(storesReloadInterval, sslCtxFactory));
481+
}
482+
469483
listener = new ServerConnector(server.webServer,
470484
new SslConnectionFactory(sslCtxFactory, HttpVersion.HTTP_1_1.toString()),
471485
new HttpConnectionFactory(httpsConfig));
@@ -496,6 +510,25 @@ public HttpServer build() throws IOException {
496510

497511
}
498512

513+
private Timer makeConfigurationChangeMonitor(long reloadInterval,
514+
SslContextFactory.Server sslContextFactory) {
515+
Timer timer = new Timer("SSL Certificates Store Monitor", true);
516+
//
517+
// The Jetty SSLContextFactory provides a 'reload' method which will reload both
518+
// truststore and keystore certificates.
519+
//
520+
timer.schedule(new FileMonitoringTimerTask(Paths.get(keyStore), path -> {
521+
LOG.info("Reloading certificates from store keystore " + keyStore);
522+
try {
523+
sslContextFactory.reload(factory -> {
524+
});
525+
} catch (Exception ex) {
526+
LOG.error("Failed to reload SSL keystore certificates", ex);
527+
}
528+
}, null), reloadInterval, reloadInterval);
529+
return timer;
530+
}
531+
499532
}
500533

501534
/**
@@ -1291,6 +1324,15 @@ void openListeners() throws Exception {
12911324
*/
12921325
public void stop() throws Exception {
12931326
MultiException exception = null;
1327+
if (this.configurationChangeMonitor.isPresent()) {
1328+
try {
1329+
this.configurationChangeMonitor.get().cancel();
1330+
} catch (Exception e) {
1331+
LOG.error("Error while canceling configuration monitoring timer for webapp"
1332+
+ webAppContext.getDisplayName(), e);
1333+
exception = addMultiException(exception, e);
1334+
}
1335+
}
12941336
for (ListenerInfo li : listeners) {
12951337
if (!li.isManaged) {
12961338
continue;

hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestSSLHttpServer.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.hadoop.hbase.http;
1919

2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertNotEquals;
2122

2223
import java.io.ByteArrayOutputStream;
2324
import java.io.File;
@@ -26,6 +27,9 @@
2627
import java.net.URI;
2728
import java.net.URL;
2829
import java.security.GeneralSecurityException;
30+
import java.security.KeyPair;
31+
import java.security.KeyStore;
32+
import java.security.cert.X509Certificate;
2933
import javax.net.ssl.HttpsURLConnection;
3034
import org.apache.hadoop.conf.Configuration;
3135
import org.apache.hadoop.fs.FileUtil;
@@ -37,6 +41,7 @@
3741
import org.apache.hadoop.hbase.testclassification.MiscTests;
3842
import org.apache.hadoop.io.IOUtils;
3943
import org.apache.hadoop.net.NetUtils;
44+
import org.apache.hadoop.security.ssl.FileBasedKeyStoresFactory;
4045
import org.apache.hadoop.security.ssl.SSLFactory;
4146
import org.junit.AfterClass;
4247
import org.junit.BeforeClass;
@@ -46,6 +51,11 @@
4651
import org.slf4j.Logger;
4752
import org.slf4j.LoggerFactory;
4853

54+
import org.apache.hbase.thirdparty.org.eclipse.jetty.server.AbstractConnector;
55+
import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ConnectionFactory;
56+
import org.apache.hbase.thirdparty.org.eclipse.jetty.server.SslConnectionFactory;
57+
import org.apache.hbase.thirdparty.org.eclipse.jetty.util.ssl.SslContextFactory;
58+
4959
/**
5060
* This testcase issues SSL certificates configures the HttpServer to serve HTTPS using the created
5161
* certficates and calls an echo servlet using the corresponding HTTPS URL.
@@ -65,6 +75,7 @@ public class TestSSLHttpServer extends HttpServerFunctionalTest {
6575
private static String sslConfDir;
6676
private static SSLFactory clientSslFactory;
6777
private static HBaseCommonTestingUtil HTU;
78+
private static long reloadInterval;
6879

6980
@BeforeClass
7081
public static void setup() throws Exception {
@@ -74,6 +85,9 @@ public static void setup() throws Exception {
7485

7586
serverConf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
7687
serverConf.setBoolean(ServerConfigurationKeys.HBASE_SSL_ENABLED_KEY, true);
88+
reloadInterval = 1000;
89+
serverConf.setLong(FileBasedKeyStoresFactory.SSL_STORES_RELOAD_INTERVAL_TPL_KEY,
90+
reloadInterval);
7791

7892
keystoresDir = new File(HTU.getDataTestDir("keystore").toString());
7993
keystoresDir.mkdirs();
@@ -131,6 +145,45 @@ public void testSecurityHeaders() throws IOException, GeneralSecurityException {
131145
conn.getHeaderField("Content-Security-Policy"));
132146
}
133147

148+
@Test(timeout = 60000)
149+
public void testReloadKeyStore() throws Exception {
150+
String serverKS = keystoresDir + "/serverKS.jks";
151+
String serverPassword = "serverP";
152+
153+
KeyStore oldKeyStore = KeyStoreTestUtil.loadKeyStore(serverKS, serverPassword.toCharArray());
154+
155+
KeyPair sKP = KeyStoreTestUtil.generateKeyPair("RSA");
156+
X509Certificate sCert =
157+
KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", sKP, 30, "SHA1withRSA");
158+
KeyStoreTestUtil.createKeyStore(serverKS, serverPassword, "server", sKP.getPrivate(), sCert);
159+
KeyStore newKeyStore = KeyStoreTestUtil.loadKeyStore(serverKS, serverPassword.toCharArray());
160+
161+
Thread.sleep((reloadInterval + 1000));
162+
163+
for (AbstractConnector connector : server.getServerConnectors()) {
164+
if (connector != null) {
165+
for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
166+
if (connectionFactory instanceof SslConnectionFactory) {
167+
SslContextFactory sslContextFactory =
168+
((SslConnectionFactory) connectionFactory).getSslContextFactory();
169+
KeyStore currentKeyStore = sslContextFactory.getKeyStore();
170+
171+
assertNotEquals(currentKeyStore.getCertificate("server"),
172+
oldKeyStore.getCertificate("server"));
173+
assertNotEquals(currentKeyStore.getKey("server", serverPassword.toCharArray()),
174+
oldKeyStore.getKey("server", serverPassword.toCharArray()));
175+
176+
assertEquals(currentKeyStore.getCertificate("server"),
177+
newKeyStore.getCertificate("server"));
178+
assertEquals(currentKeyStore.getKey("server", serverPassword.toCharArray()),
179+
newKeyStore.getKey("server", serverPassword.toCharArray()));
180+
181+
}
182+
}
183+
}
184+
}
185+
}
186+
134187
private static String readOut(URL url) throws Exception {
135188
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
136189
conn.setSSLSocketFactory(clientSslFactory.createSSLSocketFactory());

hbase-http/src/test/java/org/apache/hadoop/hbase/http/ssl/KeyStoreTestUtil.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.File;
2121
import java.io.FileOutputStream;
2222
import java.io.IOException;
23+
import java.io.InputStream;
2324
import java.io.Writer;
2425
import java.math.BigInteger;
2526
import java.net.URL;
@@ -173,6 +174,34 @@ public static void createKeyStore(String filename, String password, String keyPa
173174
saveKeyStore(ks, filename, password);
174175
}
175176

177+
/**
178+
* Load a keystore from the jks file.
179+
* @param filename String filename to load keystore
180+
* @param password char array password to load keystore
181+
* @throws GeneralSecurityException for any error with the security APIs
182+
* @throws IOException if there is an I/O error saving the file
183+
*/
184+
public static KeyStore loadKeyStore(String filename, char[] password)
185+
throws GeneralSecurityException, IOException {
186+
return loadKeyStore(filename, password, "JKS");
187+
}
188+
189+
/**
190+
* Load a keystore from the file.
191+
* @param filename String filename to load keystore
192+
* @param password char array password to load keystore
193+
* @param keystoreType String keystore file type (e.g. "JKS")
194+
* @throws GeneralSecurityException for any error with the security APIs
195+
* @throws IOException if there is an I/O error saving the file
196+
*/
197+
public static KeyStore loadKeyStore(String filename, char[] password, String keystoreType)
198+
throws GeneralSecurityException, IOException {
199+
InputStream inputStream = java.nio.file.Files.newInputStream(new File(filename).toPath());
200+
KeyStore ks = KeyStore.getInstance(keystoreType);
201+
ks.load(inputStream, password);
202+
return ks;
203+
}
204+
176205
/**
177206
* Creates a truststore with a single certificate and saves it to a file. This method uses the
178207
* default JKS truststore type.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0"?>
2+
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
3+
<!--
4+
/**
5+
*
6+
* Licensed to the Apache Software Foundation (ASF) under one
7+
* or more contributor license agreements. See the NOTICE file
8+
* distributed with this work for additional information
9+
* regarding copyright ownership. The ASF licenses this file
10+
* to you under the Apache License, Version 2.0 (the
11+
* "License"); you may not use this file except in compliance
12+
* with the License. You may obtain a copy of the License at
13+
*
14+
* http://www.apache.org/licenses/LICENSE-2.0
15+
*
16+
* Unless required by applicable law or agreed to in writing, software
17+
* distributed under the License is distributed on an "AS IS" BASIS,
18+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
* See the License for the specific language governing permissions and
20+
* limitations under the License.
21+
*/
22+
-->
23+
<configuration>
24+
<property>
25+
<name>hbase.defaults.for.version.skip</name>
26+
<value>true</value>
27+
</property>
28+
</configuration>

0 commit comments

Comments
 (0)