Skip to content

Commit e203a59

Browse files
committed
HBASE-27118 Add security headers to Thrift/HTTP server (branch-2.4) (#6156)
Signed-off-by: Duo Zhang <[email protected]> Signed-off-by: Pankaj <[email protected]> Signed-off-by: Istvan Toth <[email protected]>
1 parent 1637ad9 commit e203a59

File tree

5 files changed

+252
-26
lines changed

5 files changed

+252
-26
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717
*/
1818
package org.apache.hadoop.hbase.http;
1919

20+
import java.util.EnumSet;
21+
import javax.servlet.DispatcherType;
22+
import org.apache.hadoop.conf.Configuration;
2023
import org.apache.yetus.audience.InterfaceAudience;
2124

2225
import org.apache.hbase.thirdparty.org.eclipse.jetty.security.ConstraintMapping;
2326
import org.apache.hbase.thirdparty.org.eclipse.jetty.security.ConstraintSecurityHandler;
27+
import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.FilterHolder;
2428
import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletContextHandler;
2529
import org.apache.hbase.thirdparty.org.eclipse.jetty.util.security.Constraint;
2630

@@ -29,6 +33,9 @@
2933
*/
3034
@InterfaceAudience.Private
3135
public final class HttpServerUtil {
36+
37+
public static final String PATH_SPEC_ANY = "/*";
38+
3239
/**
3340
* Add constraints to a Jetty Context to disallow undesirable Http methods.
3441
* @param ctxHandler The context to modify
@@ -59,6 +66,24 @@ public static void constrainHttpMethods(ServletContextHandler ctxHandler,
5966
ctxHandler.setSecurityHandler(securityHandler);
6067
}
6168

69+
public static void addClickjackingPreventionFilter(ServletContextHandler ctxHandler,
70+
Configuration conf, String pathSpec) {
71+
FilterHolder holder = new FilterHolder();
72+
holder.setName("clickjackingprevention");
73+
holder.setClassName(ClickjackingPreventionFilter.class.getName());
74+
holder.setInitParameters(ClickjackingPreventionFilter.getDefaultParameters(conf));
75+
ctxHandler.addFilter(holder, pathSpec, EnumSet.allOf(DispatcherType.class));
76+
}
77+
78+
public static void addSecurityHeadersFilter(ServletContextHandler ctxHandler, Configuration conf,
79+
boolean isSecure, String pathSpec) {
80+
FilterHolder holder = new FilterHolder();
81+
holder.setName("securityheaders");
82+
holder.setClassName(SecurityHeadersFilter.class.getName());
83+
holder.setInitParameters(SecurityHeadersFilter.getDefaultParameters(conf, isSecure));
84+
ctxHandler.addFilter(holder, pathSpec, EnumSet.allOf(DispatcherType.class));
85+
}
86+
6287
private HttpServerUtil() {
6388
}
6489
}

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818
package org.apache.hadoop.hbase.rest;
1919

20+
import static org.apache.hadoop.hbase.http.HttpServerUtil.PATH_SPEC_ANY;
21+
2022
import java.lang.management.ManagementFactory;
2123
import java.util.ArrayList;
2224
import java.util.EnumSet;
@@ -29,10 +31,8 @@
2931
import org.apache.hadoop.conf.Configuration;
3032
import org.apache.hadoop.hbase.HBaseConfiguration;
3133
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
32-
import org.apache.hadoop.hbase.http.ClickjackingPreventionFilter;
3334
import org.apache.hadoop.hbase.http.HttpServerUtil;
3435
import org.apache.hadoop.hbase.http.InfoServer;
35-
import org.apache.hadoop.hbase.http.SecurityHeadersFilter;
3636
import org.apache.hadoop.hbase.log.HBaseMarkers;
3737
import org.apache.hadoop.hbase.rest.filter.AuthFilter;
3838
import org.apache.hadoop.hbase.rest.filter.GzipFilter;
@@ -95,8 +95,6 @@ public class RESTServer implements Constants {
9595
static final String HTTP_HEADER_CACHE_SIZE = "hbase.rest.http.header.cache.size";
9696
static final int DEFAULT_HTTP_HEADER_CACHE_SIZE = Character.MAX_VALUE - 1;
9797

98-
private static final String PATH_SPEC_ANY = "/*";
99-
10098
static final String REST_HTTP_ALLOW_OPTIONS_METHOD = "hbase.rest.http.allow.options.method";
10199
// HTTP OPTIONS method is commonly used in REST APIs for negotiation. So it is enabled by default.
102100
private static boolean REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT = true;
@@ -139,24 +137,6 @@ void addCSRFFilter(ServletContextHandler ctxHandler, Configuration conf) {
139137
}
140138
}
141139

142-
private void addClickjackingPreventionFilter(ServletContextHandler ctxHandler,
143-
Configuration conf) {
144-
FilterHolder holder = new FilterHolder();
145-
holder.setName("clickjackingprevention");
146-
holder.setClassName(ClickjackingPreventionFilter.class.getName());
147-
holder.setInitParameters(ClickjackingPreventionFilter.getDefaultParameters(conf));
148-
ctxHandler.addFilter(holder, PATH_SPEC_ANY, EnumSet.allOf(DispatcherType.class));
149-
}
150-
151-
private void addSecurityHeadersFilter(ServletContextHandler ctxHandler, Configuration conf,
152-
boolean isSecure) {
153-
FilterHolder holder = new FilterHolder();
154-
holder.setName("securityheaders");
155-
holder.setClassName(SecurityHeadersFilter.class.getName());
156-
holder.setInitParameters(SecurityHeadersFilter.getDefaultParameters(conf, isSecure));
157-
ctxHandler.addFilter(holder, PATH_SPEC_ANY, EnumSet.allOf(DispatcherType.class));
158-
}
159-
160140
// login the server principal (if using secure Hadoop)
161141
private static Pair<FilterHolder, Class<? extends ServletContainer>>
162142
loginServerPrincipal(UserProvider userProvider, Configuration conf) throws Exception {
@@ -396,8 +376,8 @@ public synchronized void run() throws Exception {
396376
ctxHandler.addFilter(filter, PATH_SPEC_ANY, EnumSet.of(DispatcherType.REQUEST));
397377
}
398378
addCSRFFilter(ctxHandler, conf);
399-
addClickjackingPreventionFilter(ctxHandler, conf);
400-
addSecurityHeadersFilter(ctxHandler, conf, isSecure);
379+
HttpServerUtil.addClickjackingPreventionFilter(ctxHandler, conf, PATH_SPEC_ANY);
380+
HttpServerUtil.addSecurityHeadersFilter(ctxHandler, conf, isSecure, PATH_SPEC_ANY);
401381
HttpServerUtil.constrainHttpMethods(ctxHandler, servlet.getConfiguration()
402382
.getBoolean(REST_HTTP_ALLOW_OPTIONS_METHOD, REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT));
403383

hbase-thrift/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@
149149
<artifactId>reload4j</artifactId>
150150
<scope>test</scope>
151151
</dependency>
152+
<dependency>
153+
<groupId>org.bouncycastle</groupId>
154+
<artifactId>bcprov-jdk18on</artifactId>
155+
<scope>test</scope>
156+
</dependency>
152157
</dependencies>
153158

154159
<build>

hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServer.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package org.apache.hadoop.hbase.thrift;
1919

20+
import static org.apache.hadoop.hbase.http.HttpServerUtil.PATH_SPEC_ANY;
2021
import static org.apache.hadoop.hbase.thrift.Constants.BACKLOG_CONF_DEAFULT;
2122
import static org.apache.hadoop.hbase.thrift.Constants.BACKLOG_CONF_KEY;
2223
import static org.apache.hadoop.hbase.thrift.Constants.BIND_CONF_KEY;
@@ -386,9 +387,12 @@ protected void setupHTTPServer() throws IOException {
386387
httpServer = new Server(threadPool);
387388

388389
// Context handler
390+
boolean isSecure = conf.getBoolean(THRIFT_SSL_ENABLED_KEY, false);
389391
ServletContextHandler ctxHandler =
390392
new ServletContextHandler(httpServer, "/", ServletContextHandler.SESSIONS);
391-
ctxHandler.addServlet(new ServletHolder(thriftHttpServlet), "/*");
393+
HttpServerUtil.addClickjackingPreventionFilter(ctxHandler, conf, PATH_SPEC_ANY);
394+
HttpServerUtil.addSecurityHeadersFilter(ctxHandler, conf, isSecure, PATH_SPEC_ANY);
395+
ctxHandler.addServlet(new ServletHolder(thriftHttpServlet), PATH_SPEC_ANY);
392396
HttpServerUtil.constrainHttpMethods(ctxHandler,
393397
conf.getBoolean(THRIFT_HTTP_ALLOW_OPTIONS_METHOD, THRIFT_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT));
394398

@@ -403,7 +407,7 @@ protected void setupHTTPServer() throws IOException {
403407
httpConfig.setSendDateHeader(false);
404408

405409
ServerConnector serverConnector;
406-
if (conf.getBoolean(THRIFT_SSL_ENABLED_KEY, false)) {
410+
if (isSecure) {
407411
HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
408412
httpsConfig.addCustomizer(new SecureRequestCustomizer());
409413

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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.hadoop.hbase.thrift;
19+
20+
import static org.apache.hadoop.hbase.thrift.TestThriftServerCmdLine.createBoundServer;
21+
import static org.junit.Assert.assertEquals;
22+
23+
import java.io.BufferedInputStream;
24+
import java.io.File;
25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.lang.reflect.Method;
28+
import java.net.HttpURLConnection;
29+
import java.nio.file.Files;
30+
import java.security.KeyPair;
31+
import java.security.KeyStore;
32+
import java.security.cert.X509Certificate;
33+
import javax.net.ssl.SSLContext;
34+
import org.apache.hadoop.conf.Configuration;
35+
import org.apache.hadoop.hbase.HBaseClassTestRule;
36+
import org.apache.hadoop.hbase.HBaseTestingUtility;
37+
import org.apache.hadoop.hbase.HConstants;
38+
import org.apache.hadoop.hbase.testclassification.ClientTests;
39+
import org.apache.hadoop.hbase.testclassification.LargeTests;
40+
import org.apache.hadoop.hbase.thrift.generated.Hbase;
41+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
42+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper;
43+
import org.apache.hadoop.hbase.util.IncrementingEnvironmentEdge;
44+
import org.apache.hadoop.hbase.util.TableDescriptorChecker;
45+
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
46+
import org.apache.http.client.methods.CloseableHttpResponse;
47+
import org.apache.http.client.methods.HttpPost;
48+
import org.apache.http.entity.ByteArrayEntity;
49+
import org.apache.http.impl.client.CloseableHttpClient;
50+
import org.apache.http.impl.client.HttpClientBuilder;
51+
import org.apache.http.impl.client.HttpClients;
52+
import org.apache.http.ssl.SSLContexts;
53+
import org.apache.thrift.protocol.TBinaryProtocol;
54+
import org.apache.thrift.protocol.TProtocol;
55+
import org.apache.thrift.transport.TMemoryBuffer;
56+
import org.junit.After;
57+
import org.junit.AfterClass;
58+
import org.junit.Before;
59+
import org.junit.BeforeClass;
60+
import org.junit.ClassRule;
61+
import org.junit.Test;
62+
import org.junit.experimental.categories.Category;
63+
import org.slf4j.Logger;
64+
import org.slf4j.LoggerFactory;
65+
66+
@Category({ ClientTests.class, LargeTests.class })
67+
public class TestThriftHttpServerSSL {
68+
@ClassRule
69+
public static final HBaseClassTestRule CLASS_RULE =
70+
HBaseClassTestRule.forClass(TestThriftHttpServerSSL.class);
71+
72+
private static final Logger LOG = LoggerFactory.getLogger(TestThriftHttpServerSSL.class);
73+
private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
74+
private static final String KEY_STORE_PASSWORD = "myKSPassword";
75+
private static final String TRUST_STORE_PASSWORD = "myTSPassword";
76+
77+
private File keyDir;
78+
private HttpClientBuilder httpClientBuilder;
79+
private ThriftServerRunner tsr;
80+
private HttpPost httpPost = null;
81+
82+
@BeforeClass
83+
public static void setUpBeforeClass() throws Exception {
84+
TEST_UTIL.getConfiguration().setBoolean(Constants.USE_HTTP_CONF_KEY, true);
85+
TEST_UTIL.getConfiguration().setBoolean(TableDescriptorChecker.TABLE_SANITY_CHECKS, false);
86+
TEST_UTIL.startMiniCluster();
87+
// ensure that server time increments every time we do an operation, otherwise
88+
// successive puts having the same timestamp will override each other
89+
EnvironmentEdgeManagerTestHelper.injectEdge(new IncrementingEnvironmentEdge());
90+
}
91+
92+
@AfterClass
93+
public static void tearDownAfterClass() throws Exception {
94+
TEST_UTIL.shutdownMiniCluster();
95+
EnvironmentEdgeManager.reset();
96+
}
97+
98+
@Before
99+
public void setUp() throws Exception {
100+
initializeAlgorithmId();
101+
keyDir = initKeystoreDir();
102+
keyDir.deleteOnExit();
103+
KeyPair keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
104+
105+
X509Certificate serverCertificate =
106+
KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", keyPair, 30, "SHA1withRSA");
107+
108+
generateTrustStore(serverCertificate);
109+
generateKeyStore(keyPair, serverCertificate);
110+
111+
Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
112+
conf.setBoolean(Constants.THRIFT_SSL_ENABLED_KEY, true);
113+
conf.set(Constants.THRIFT_SSL_KEYSTORE_STORE_KEY, getKeystoreFilePath());
114+
conf.set(Constants.THRIFT_SSL_KEYSTORE_PASSWORD_KEY, KEY_STORE_PASSWORD);
115+
conf.set(Constants.THRIFT_SSL_KEYSTORE_KEYPASSWORD_KEY, KEY_STORE_PASSWORD);
116+
117+
tsr = createBoundServer(() -> new ThriftServer(conf));
118+
String url = "https://" + HConstants.LOCALHOST + ":" + tsr.getThriftServer().listenPort;
119+
120+
KeyStore trustStore;
121+
trustStore = KeyStore.getInstance("JKS");
122+
try (InputStream inputStream =
123+
new BufferedInputStream(Files.newInputStream(new File(getTruststoreFilePath()).toPath()))) {
124+
trustStore.load(inputStream, TRUST_STORE_PASSWORD.toCharArray());
125+
}
126+
127+
httpClientBuilder = HttpClients.custom();
128+
SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(trustStore, null).build();
129+
httpClientBuilder.setSSLContext(sslcontext);
130+
131+
httpPost = new HttpPost(url);
132+
httpPost.setHeader("Content-Type", "application/x-thrift");
133+
httpPost.setHeader("Accept", "application/x-thrift");
134+
httpPost.setHeader("User-Agent", "Java/THttpClient/HC");
135+
}
136+
137+
@After
138+
public void tearDown() throws IOException {
139+
if (httpPost != null) {
140+
httpPost.releaseConnection();
141+
}
142+
if (tsr != null) {
143+
tsr.close();
144+
}
145+
}
146+
147+
@Test
148+
public void testSecurityHeaders() throws Exception {
149+
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
150+
TMemoryBuffer memoryBuffer = new TMemoryBuffer(100);
151+
TProtocol prot = new TBinaryProtocol(memoryBuffer);
152+
Hbase.Client client = new Hbase.Client(prot);
153+
client.send_getClusterId();
154+
155+
httpPost.setEntity(new ByteArrayEntity(memoryBuffer.getArray()));
156+
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
157+
158+
assertEquals(HttpURLConnection.HTTP_OK, httpResponse.getStatusLine().getStatusCode());
159+
assertEquals("DENY", httpResponse.getFirstHeader("X-Frame-Options").getValue());
160+
161+
assertEquals("nosniff", httpResponse.getFirstHeader("X-Content-Type-Options").getValue());
162+
assertEquals("1; mode=block", httpResponse.getFirstHeader("X-XSS-Protection").getValue());
163+
164+
assertEquals("default-src https: data: 'unsafe-inline' 'unsafe-eval'",
165+
httpResponse.getFirstHeader("Content-Security-Policy").getValue());
166+
assertEquals("max-age=63072000;includeSubDomains;preload",
167+
httpResponse.getFirstHeader("Strict-Transport-Security").getValue());
168+
}
169+
}
170+
171+
// Workaround for jdk8 292 bug. See https://github.com/bcgit/bc-java/issues/941
172+
// Below is a workaround described in above URL. Issue fingered first in comments in
173+
// HBASE-25920 Support Hadoop 3.3.1
174+
private static void initializeAlgorithmId() {
175+
try {
176+
Class<?> algoId = Class.forName("sun.security.x509.AlgorithmId");
177+
Method method = algoId.getMethod("get", String.class);
178+
method.setAccessible(true);
179+
method.invoke(null, "PBEWithSHA1AndDESede");
180+
} catch (Exception e) {
181+
LOG.warn("failed to initialize AlgorithmId", e);
182+
}
183+
}
184+
185+
private File initKeystoreDir() {
186+
String dataTestDir = TEST_UTIL.getDataTestDir().toString();
187+
File keystoreDir = new File(dataTestDir, TestThriftHttpServer.class.getSimpleName() + "_keys");
188+
keystoreDir.mkdirs();
189+
return keystoreDir;
190+
}
191+
192+
private void generateKeyStore(KeyPair keyPair, X509Certificate serverCertificate)
193+
throws Exception {
194+
String keyStorePath = getKeystoreFilePath();
195+
KeyStoreTestUtil.createKeyStore(keyStorePath, KEY_STORE_PASSWORD, KEY_STORE_PASSWORD,
196+
"serverKS", keyPair.getPrivate(), serverCertificate);
197+
}
198+
199+
private void generateTrustStore(X509Certificate serverCertificate) throws Exception {
200+
String trustStorePath = getTruststoreFilePath();
201+
KeyStoreTestUtil.createTrustStore(trustStorePath, TRUST_STORE_PASSWORD, "serverTS",
202+
serverCertificate);
203+
}
204+
205+
private String getKeystoreFilePath() {
206+
return String.format("%s/serverKS.%s", keyDir.getAbsolutePath(), "jks");
207+
}
208+
209+
private String getTruststoreFilePath() {
210+
return String.format("%s/serverTS.%s", keyDir.getAbsolutePath(), "jks");
211+
}
212+
}

0 commit comments

Comments
 (0)