HBASE-5291 Add Kerberos HTTP SPNEGO authentication support to HBase web consoles (Josh Elser)
This commit is contained in:
parent
6c60bc9f6c
commit
ae5fe1e616
|
@ -572,6 +572,28 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.kerby</groupId>
|
||||||
|
<artifactId>kerb-client</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.kerby</groupId>
|
||||||
|
<artifactId>kerb-simplekdc</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
<!-- Overriden to get some SPNEGO classes only in newer version -->
|
||||||
|
<version>4.5.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpcore</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<profiles>
|
<profiles>
|
||||||
<!-- Needs to make the profile in apache parent pom -->
|
<!-- Needs to make the profile in apache parent pom -->
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -102,11 +103,30 @@ import com.sun.jersey.spi.container.servlet.ServletContainer;
|
||||||
@InterfaceStability.Evolving
|
@InterfaceStability.Evolving
|
||||||
public class HttpServer implements FilterContainer {
|
public class HttpServer implements FilterContainer {
|
||||||
private static final Log LOG = LogFactory.getLog(HttpServer.class);
|
private static final Log LOG = LogFactory.getLog(HttpServer.class);
|
||||||
|
private static final String EMPTY_STRING = "";
|
||||||
|
|
||||||
static final String FILTER_INITIALIZERS_PROPERTY
|
static final String FILTER_INITIALIZERS_PROPERTY
|
||||||
= "hbase.http.filter.initializers";
|
= "hbase.http.filter.initializers";
|
||||||
static final String HTTP_MAX_THREADS = "hbase.http.max.threads";
|
static final String HTTP_MAX_THREADS = "hbase.http.max.threads";
|
||||||
|
|
||||||
|
public static final String HTTP_UI_AUTHENTICATION = "hbase.security.authentication.ui";
|
||||||
|
static final String HTTP_AUTHENTICATION_PREFIX = "hbase.security.authentication.spnego.";
|
||||||
|
static final String HTTP_SPNEGO_AUTHENTICATION_PREFIX = HTTP_AUTHENTICATION_PREFIX
|
||||||
|
+ "spnego.";
|
||||||
|
static final String HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX = "kerberos.principal";
|
||||||
|
public static final String HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY =
|
||||||
|
HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX;
|
||||||
|
static final String HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX = "kerberos.keytab";
|
||||||
|
public static final String HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY =
|
||||||
|
HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX;
|
||||||
|
static final String HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_SUFFIX = "kerberos.name.rules";
|
||||||
|
public static final String HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_KEY =
|
||||||
|
HTTP_SPNEGO_AUTHENTICATION_PREFIX + HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_SUFFIX;
|
||||||
|
static final String HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_SUFFIX =
|
||||||
|
"signature.secret.file";
|
||||||
|
public static final String HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_KEY =
|
||||||
|
HTTP_AUTHENTICATION_PREFIX + HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_SUFFIX;
|
||||||
|
|
||||||
// The ServletContext attribute where the daemon Configuration
|
// The ServletContext attribute where the daemon Configuration
|
||||||
// gets stored.
|
// gets stored.
|
||||||
public static final String CONF_CONTEXT_ATTRIBUTE = "hbase.conf";
|
public static final String CONF_CONTEXT_ATTRIBUTE = "hbase.conf";
|
||||||
|
@ -175,6 +195,9 @@ public class HttpServer implements FilterContainer {
|
||||||
// The -keypass option in keytool
|
// The -keypass option in keytool
|
||||||
private String keyPassword;
|
private String keyPassword;
|
||||||
|
|
||||||
|
private String kerberosNameRulesKey;
|
||||||
|
private String signatureSecretFileKey;
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
private String name;
|
private String name;
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
@ -302,6 +325,16 @@ public class HttpServer implements FilterContainer {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setKerberosNameRulesKey(String kerberosNameRulesKey) {
|
||||||
|
this.kerberosNameRulesKey = kerberosNameRulesKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setSignatureSecretFileKey(String signatureSecretFileKey) {
|
||||||
|
this.signatureSecretFileKey = signatureSecretFileKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder setAppDir(String appDir) {
|
public Builder setAppDir(String appDir) {
|
||||||
this.appDir = appDir;
|
this.appDir = appDir;
|
||||||
return this;
|
return this;
|
||||||
|
@ -344,7 +377,8 @@ public class HttpServer implements FilterContainer {
|
||||||
HttpServer server = new HttpServer(this);
|
HttpServer server = new HttpServer(this);
|
||||||
|
|
||||||
if (this.securityEnabled) {
|
if (this.securityEnabled) {
|
||||||
server.initSpnego(conf, hostName, usernameConfKey, keytabConfKey);
|
server.initSpnego(conf, hostName, usernameConfKey, keytabConfKey, kerberosNameRulesKey,
|
||||||
|
signatureSecretFileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connector != null) {
|
if (connector != null) {
|
||||||
|
@ -927,21 +961,60 @@ public class HttpServer implements FilterContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSpnego(Configuration conf, String hostName,
|
private void initSpnego(Configuration conf, String hostName,
|
||||||
String usernameConfKey, String keytabConfKey) throws IOException {
|
String usernameConfKey, String keytabConfKey, String kerberosNameRuleKey,
|
||||||
|
String signatureSecretKeyFileKey) throws IOException {
|
||||||
Map<String, String> params = new HashMap<String, String>();
|
Map<String, String> params = new HashMap<String, String>();
|
||||||
String principalInConf = conf.get(usernameConfKey);
|
String principalInConf = getOrEmptyString(conf, usernameConfKey);
|
||||||
if (principalInConf != null && !principalInConf.isEmpty()) {
|
if (!principalInConf.isEmpty()) {
|
||||||
params.put("kerberos.principal", SecurityUtil.getServerPrincipal(
|
params.put(HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX, SecurityUtil.getServerPrincipal(
|
||||||
principalInConf, hostName));
|
principalInConf, hostName));
|
||||||
}
|
}
|
||||||
String httpKeytab = conf.get(keytabConfKey);
|
String httpKeytab = getOrEmptyString(conf, keytabConfKey);
|
||||||
if (httpKeytab != null && !httpKeytab.isEmpty()) {
|
if (!httpKeytab.isEmpty()) {
|
||||||
params.put("kerberos.keytab", httpKeytab);
|
params.put(HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX, httpKeytab);
|
||||||
|
}
|
||||||
|
String kerberosNameRule = getOrEmptyString(conf, kerberosNameRuleKey);
|
||||||
|
if (!kerberosNameRule.isEmpty()) {
|
||||||
|
params.put(HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_SUFFIX, kerberosNameRule);
|
||||||
|
}
|
||||||
|
String signatureSecretKeyFile = getOrEmptyString(conf, signatureSecretKeyFileKey);
|
||||||
|
if (!signatureSecretKeyFile.isEmpty()) {
|
||||||
|
params.put(HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_SUFFIX,
|
||||||
|
signatureSecretKeyFile);
|
||||||
}
|
}
|
||||||
params.put(AuthenticationFilter.AUTH_TYPE, "kerberos");
|
params.put(AuthenticationFilter.AUTH_TYPE, "kerberos");
|
||||||
|
|
||||||
defineFilter(webAppContext, SPNEGO_FILTER,
|
// Verify that the required options were provided
|
||||||
AuthenticationFilter.class.getName(), params, null);
|
if (isMissing(params.get(HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX)) ||
|
||||||
|
isMissing(params.get(HTTP_SPNEGO_AUTHENTICATION_KEYTAB_SUFFIX))) {
|
||||||
|
throw new IllegalArgumentException(usernameConfKey + " and "
|
||||||
|
+ keytabConfKey + " are both required in the configuration "
|
||||||
|
+ "to enable SPNEGO/Kerberos authentication for the Web UI");
|
||||||
|
}
|
||||||
|
|
||||||
|
addGlobalFilter(SPNEGO_FILTER, AuthenticationFilter.class.getName(), params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the argument is non-null and not whitespace
|
||||||
|
*/
|
||||||
|
private boolean isMissing(String value) {
|
||||||
|
if (null == value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return value.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the value for the given key from the configuration of returns a string of
|
||||||
|
* zero length.
|
||||||
|
*/
|
||||||
|
private String getOrEmptyString(Configuration conf, String key) {
|
||||||
|
if (null == key) {
|
||||||
|
return EMPTY_STRING;
|
||||||
|
}
|
||||||
|
final String value = conf.get(key.trim());
|
||||||
|
return null == value ? EMPTY_STRING : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -75,6 +75,15 @@ public class InfoServer {
|
||||||
HBaseConfiguration.getPassword(c, "ssl.server.truststore.password", null),
|
HBaseConfiguration.getPassword(c, "ssl.server.truststore.password", null),
|
||||||
c.get("ssl.server.truststore.type", "jks"));
|
c.get("ssl.server.truststore.type", "jks"));
|
||||||
}
|
}
|
||||||
|
// Enable SPNEGO authentication
|
||||||
|
if ("kerberos".equalsIgnoreCase(c.get(HttpServer.HTTP_UI_AUTHENTICATION, null))) {
|
||||||
|
builder.setUsernameConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY)
|
||||||
|
.setKeytabConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY)
|
||||||
|
.setKerberosNameRulesKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_KEY)
|
||||||
|
.setSignatureSecretFileKey(
|
||||||
|
HttpServer.HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_KEY)
|
||||||
|
.setSecurityEnabled(true);
|
||||||
|
}
|
||||||
this.httpServer = builder.build();
|
this.httpServer = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ import org.apache.hadoop.hbase.exceptions.UnknownProtocolException;
|
||||||
import org.apache.hadoop.hbase.executor.ExecutorService;
|
import org.apache.hadoop.hbase.executor.ExecutorService;
|
||||||
import org.apache.hadoop.hbase.executor.ExecutorType;
|
import org.apache.hadoop.hbase.executor.ExecutorType;
|
||||||
import org.apache.hadoop.hbase.fs.HFileSystem;
|
import org.apache.hadoop.hbase.fs.HFileSystem;
|
||||||
|
import org.apache.hadoop.hbase.http.HttpServer;
|
||||||
import org.apache.hadoop.hbase.http.InfoServer;
|
import org.apache.hadoop.hbase.http.InfoServer;
|
||||||
import org.apache.hadoop.hbase.io.hfile.CacheConfig;
|
import org.apache.hadoop.hbase.io.hfile.CacheConfig;
|
||||||
import org.apache.hadoop.hbase.io.hfile.HFile;
|
import org.apache.hadoop.hbase.io.hfile.HFile;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.apache.hadoop.hbase.http.HttpServer.Builder;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.ServerSocket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
@ -94,6 +95,15 @@ public class HttpServerFunctionalTest extends Assert {
|
||||||
return createServer(TEST, conf, pathSpecs);
|
return createServer(TEST, conf, pathSpecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HttpServer createTestServerWithSecurity(Configuration conf) throws IOException {
|
||||||
|
prepareTestWebapp();
|
||||||
|
return localServerBuilder(TEST).setFindPort(true).setConf(conf).setSecurityEnabled(true)
|
||||||
|
// InfoServer normally sets these for us
|
||||||
|
.setUsernameConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY)
|
||||||
|
.setKeytabConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the test webapp by creating the directory from the test properties
|
* Prepare the test webapp by creating the directory from the test properties
|
||||||
* fail if the directory cannot be created.
|
* fail if the directory cannot be created.
|
||||||
|
@ -226,4 +236,37 @@ public class HttpServerFunctionalTest extends Assert {
|
||||||
}
|
}
|
||||||
return out.toString();
|
return out.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively deletes a {@link File}.
|
||||||
|
*/
|
||||||
|
protected static void deleteRecursively(File d) {
|
||||||
|
if (d.isDirectory()) {
|
||||||
|
for (String name : d.list()) {
|
||||||
|
File child = new File(d, name);
|
||||||
|
if (child.isFile()) {
|
||||||
|
child.delete();
|
||||||
|
} else {
|
||||||
|
deleteRecursively(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks a free port on the host by binding a Socket to '0'.
|
||||||
|
*/
|
||||||
|
protected static int getFreePort() throws IOException {
|
||||||
|
ServerSocket s = new ServerSocket(0);
|
||||||
|
try {
|
||||||
|
s.setReuseAddress(true);
|
||||||
|
int port = s.getLocalPort();
|
||||||
|
return port;
|
||||||
|
} finally {
|
||||||
|
if (null != s) {
|
||||||
|
s.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,258 @@
|
||||||
|
/*
|
||||||
|
* 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.hadoop.hbase.http;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.PrivilegedExceptionAction;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.security.auth.Subject;
|
||||||
|
import javax.security.auth.kerberos.KerberosTicket;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.http.TestHttpServer.EchoServlet;
|
||||||
|
import org.apache.hadoop.hbase.http.resource.JerseyResource;
|
||||||
|
import org.apache.hadoop.hbase.testclassification.MiscTests;
|
||||||
|
import org.apache.hadoop.hbase.testclassification.SmallTests;
|
||||||
|
import org.apache.hadoop.security.authentication.util.KerberosName;
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.auth.AuthSchemeProvider;
|
||||||
|
import org.apache.http.auth.AuthScope;
|
||||||
|
import org.apache.http.auth.KerberosCredentials;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.config.AuthSchemes;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.client.protocol.HttpClientContext;
|
||||||
|
import org.apache.http.config.Lookup;
|
||||||
|
import org.apache.http.config.RegistryBuilder;
|
||||||
|
import org.apache.http.entity.ByteArrayEntity;
|
||||||
|
import org.apache.http.entity.ContentType;
|
||||||
|
import org.apache.http.impl.auth.SPNegoSchemeFactory;
|
||||||
|
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.apache.kerby.kerberos.kerb.KrbException;
|
||||||
|
import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
|
||||||
|
import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
|
||||||
|
import org.ietf.jgss.GSSCredential;
|
||||||
|
import org.ietf.jgss.GSSManager;
|
||||||
|
import org.ietf.jgss.GSSName;
|
||||||
|
import org.ietf.jgss.Oid;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.experimental.categories.Category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test class for SPNEGO authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
|
||||||
|
* HttpComponents to verify that a simple Servlet is reachable via SPNEGO and unreachable w/o.
|
||||||
|
*/
|
||||||
|
@Category({MiscTests.class, SmallTests.class})
|
||||||
|
public class TestSpnegoHttpServer extends HttpServerFunctionalTest {
|
||||||
|
private static final Log LOG = LogFactory.getLog(TestSpnegoHttpServer.class);
|
||||||
|
private static final String KDC_SERVER_HOST = "localhost";
|
||||||
|
private static final String CLIENT_PRINCIPAL = "client";
|
||||||
|
|
||||||
|
private static HttpServer server;
|
||||||
|
private static URL baseUrl;
|
||||||
|
private static SimpleKdcServer kdc;
|
||||||
|
private static File infoServerKeytab;
|
||||||
|
private static File clientKeytab;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupServer() throws Exception {
|
||||||
|
final String serverPrincipal = "HTTP/" + KDC_SERVER_HOST;
|
||||||
|
final File target = new File(System.getProperty("user.dir"), "target");
|
||||||
|
assertTrue(target.exists());
|
||||||
|
|
||||||
|
kdc = buildMiniKdc();
|
||||||
|
kdc.start();
|
||||||
|
|
||||||
|
File keytabDir = new File(target, TestSpnegoHttpServer.class.getSimpleName()
|
||||||
|
+ "_keytabs");
|
||||||
|
if (keytabDir.exists()) {
|
||||||
|
deleteRecursively(keytabDir);
|
||||||
|
}
|
||||||
|
keytabDir.mkdirs();
|
||||||
|
|
||||||
|
infoServerKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
|
||||||
|
clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
|
||||||
|
|
||||||
|
setupUser(kdc, clientKeytab, CLIENT_PRINCIPAL);
|
||||||
|
setupUser(kdc, infoServerKeytab, serverPrincipal);
|
||||||
|
|
||||||
|
Configuration conf = buildSpnegoConfiguration(serverPrincipal, infoServerKeytab);
|
||||||
|
|
||||||
|
server = createTestServerWithSecurity(conf);
|
||||||
|
server.addServlet("echo", "/echo", EchoServlet.class);
|
||||||
|
server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
|
||||||
|
server.start();
|
||||||
|
baseUrl = getServerURL(server);
|
||||||
|
|
||||||
|
LOG.info("HTTP server started: "+ baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void stopServer() throws Exception {
|
||||||
|
try {
|
||||||
|
if (null != server) {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.info("Failed to stop info server", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (null != kdc) {
|
||||||
|
kdc.stop();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.info("Failed to stop mini KDC", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
|
||||||
|
throws KrbException {
|
||||||
|
kdc.createPrincipal(principal);
|
||||||
|
kdc.exportPrincipal(principal, keytab);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SimpleKdcServer buildMiniKdc() throws Exception {
|
||||||
|
SimpleKdcServer kdc = new SimpleKdcServer();
|
||||||
|
|
||||||
|
final File target = new File(System.getProperty("user.dir"), "target");
|
||||||
|
File kdcDir = new File(target, TestSpnegoHttpServer.class.getSimpleName());
|
||||||
|
if (kdcDir.exists()) {
|
||||||
|
deleteRecursively(kdcDir);
|
||||||
|
}
|
||||||
|
kdcDir.mkdirs();
|
||||||
|
kdc.setWorkDir(kdcDir);
|
||||||
|
|
||||||
|
kdc.setKdcHost(KDC_SERVER_HOST);
|
||||||
|
int kdcPort = getFreePort();
|
||||||
|
kdc.setAllowTcp(true);
|
||||||
|
kdc.setAllowUdp(false);
|
||||||
|
kdc.setKdcTcpPort(kdcPort);
|
||||||
|
|
||||||
|
LOG.info("Starting KDC server at " + KDC_SERVER_HOST + ":" + kdcPort);
|
||||||
|
|
||||||
|
kdc.init();
|
||||||
|
|
||||||
|
return kdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Configuration buildSpnegoConfiguration(String serverPrincipal, File
|
||||||
|
serverKeytab) {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
KerberosName.setRules("DEFAULT");
|
||||||
|
|
||||||
|
conf.setInt(HttpServer.HTTP_MAX_THREADS, 10);
|
||||||
|
|
||||||
|
// Enable Kerberos (pre-req)
|
||||||
|
conf.set("hbase.security.authentication", "kerberos");
|
||||||
|
conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
|
||||||
|
conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, serverPrincipal);
|
||||||
|
conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, serverKeytab.getAbsolutePath());
|
||||||
|
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnauthorizedClientsDisallowed() throws IOException {
|
||||||
|
URL url = new URL(getServerURL(server), "/echo?a=b");
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAllowedClient() throws Exception {
|
||||||
|
// Create the subject for the client
|
||||||
|
final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(CLIENT_PRINCIPAL, clientKeytab);
|
||||||
|
final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
|
||||||
|
// Make sure the subject has a principal
|
||||||
|
assertFalse(clientPrincipals.isEmpty());
|
||||||
|
|
||||||
|
// Get a TGT for the subject (might have many, different encryption types). The first should
|
||||||
|
// be the default encryption type.
|
||||||
|
Set<KerberosTicket> privateCredentials =
|
||||||
|
clientSubject.getPrivateCredentials(KerberosTicket.class);
|
||||||
|
assertFalse(privateCredentials.isEmpty());
|
||||||
|
KerberosTicket tgt = privateCredentials.iterator().next();
|
||||||
|
assertNotNull(tgt);
|
||||||
|
|
||||||
|
// The name of the principal
|
||||||
|
final String principalName = clientPrincipals.iterator().next().getName();
|
||||||
|
|
||||||
|
// Run this code, logged in as the subject (the client)
|
||||||
|
HttpResponse resp = Subject.doAs(clientSubject,
|
||||||
|
new PrivilegedExceptionAction<HttpResponse>() {
|
||||||
|
@Override
|
||||||
|
public HttpResponse run() throws Exception {
|
||||||
|
// Logs in with Kerberos via GSS
|
||||||
|
GSSManager gssManager = GSSManager.getInstance();
|
||||||
|
// jGSS Kerberos login constant
|
||||||
|
Oid oid = new Oid("1.2.840.113554.1.2.2");
|
||||||
|
GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
|
||||||
|
GSSCredential credential = gssManager.createCredential(gssClient,
|
||||||
|
GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
|
||||||
|
|
||||||
|
HttpClientContext context = HttpClientContext.create();
|
||||||
|
Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create()
|
||||||
|
.register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpClient client = HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry).build();
|
||||||
|
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||||
|
credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
|
||||||
|
|
||||||
|
URL url = new URL(getServerURL(server), "/echo?a=b");
|
||||||
|
context.setTargetHost(new HttpHost(url.getHost(), url.getPort()));
|
||||||
|
context.setCredentialsProvider(credentialsProvider);
|
||||||
|
context.setAuthSchemeRegistry(authRegistry);
|
||||||
|
|
||||||
|
HttpGet get = new HttpGet(url.toURI());
|
||||||
|
return client.execute(get, context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(resp);
|
||||||
|
assertEquals(HttpURLConnection.HTTP_OK, resp.getStatusLine().getStatusCode());
|
||||||
|
assertEquals("a:b", EntityUtils.toString(resp.getEntity()).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testMissingConfigurationThrowsException() throws Exception {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
conf.setInt(HttpServer.HTTP_MAX_THREADS, 10);
|
||||||
|
// Enable Kerberos (pre-req)
|
||||||
|
conf.set("hbase.security.authentication", "kerberos");
|
||||||
|
// Intentionally skip keytab and principal
|
||||||
|
|
||||||
|
HttpServer customServer = createTestServerWithSecurity(conf);
|
||||||
|
customServer.addServlet("echo", "/echo", EchoServlet.class);
|
||||||
|
customServer.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
|
||||||
|
customServer.start();
|
||||||
|
}
|
||||||
|
}
|
17
pom.xml
17
pom.xml
|
@ -1231,6 +1231,7 @@
|
||||||
<!-- Do not use versions earlier than 3.2.2 due to a security vulnerability -->
|
<!-- Do not use versions earlier than 3.2.2 due to a security vulnerability -->
|
||||||
<collections.version>3.2.2</collections.version>
|
<collections.version>3.2.2</collections.version>
|
||||||
<httpclient.version>4.3.6</httpclient.version>
|
<httpclient.version>4.3.6</httpclient.version>
|
||||||
|
<httpcore.version>4.4.4</httpcore.version>
|
||||||
<metrics-core.version>3.1.2</metrics-core.version>
|
<metrics-core.version>3.1.2</metrics-core.version>
|
||||||
<guava.version>12.0.1</guava.version>
|
<guava.version>12.0.1</guava.version>
|
||||||
<jsr305.version>1.3.9</jsr305.version>
|
<jsr305.version>1.3.9</jsr305.version>
|
||||||
|
@ -1260,6 +1261,7 @@
|
||||||
<jcodings.version>1.0.8</jcodings.version>
|
<jcodings.version>1.0.8</jcodings.version>
|
||||||
<spy.version>2.11.6</spy.version>
|
<spy.version>2.11.6</spy.version>
|
||||||
<bouncycastle.version>1.46</bouncycastle.version>
|
<bouncycastle.version>1.46</bouncycastle.version>
|
||||||
|
<kerby.version>1.0.0-RC2</kerby.version>
|
||||||
<!-- Plugin Dependencies -->
|
<!-- Plugin Dependencies -->
|
||||||
<maven.assembly.version>2.4</maven.assembly.version>
|
<maven.assembly.version>2.4</maven.assembly.version>
|
||||||
<maven.antrun.version>1.8</maven.antrun.version>
|
<maven.antrun.version>1.8</maven.antrun.version>
|
||||||
|
@ -1524,6 +1526,11 @@
|
||||||
<artifactId>httpclient</artifactId>
|
<artifactId>httpclient</artifactId>
|
||||||
<version>${httpclient.version}</version>
|
<version>${httpclient.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpcore</artifactId>
|
||||||
|
<version>${httpcore.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-cli</groupId>
|
<groupId>commons-cli</groupId>
|
||||||
<artifactId>commons-cli</artifactId>
|
<artifactId>commons-cli</artifactId>
|
||||||
|
@ -1806,6 +1813,16 @@
|
||||||
<version>${bouncycastle.version}</version>
|
<version>${bouncycastle.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.kerby</groupId>
|
||||||
|
<artifactId>kerb-client</artifactId>
|
||||||
|
<version>${kerby.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.kerby</groupId>
|
||||||
|
<artifactId>kerb-simplekdc</artifactId>
|
||||||
|
<version>${kerby.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
<!-- Dependencies needed by subprojects -->
|
<!-- Dependencies needed by subprojects -->
|
||||||
|
|
|
@ -69,6 +69,59 @@ See Nick Dimiduk's contribution on this link:http://stackoverflow.com/questions/
|
||||||
If you know how to fix this without opening a second port for HTTPS, patches are appreciated.
|
If you know how to fix this without opening a second port for HTTPS, patches are appreciated.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
[[hbase.secure.spnego.ui]]
|
||||||
|
== Using SPNEGO for Kerberos authentication with Web UIs
|
||||||
|
|
||||||
|
Kerberos-authentication to HBase Web UIs can be enabled via configuring SPNEGO with the `hbase.security.authentication.ui`
|
||||||
|
property in _hbase-site.xml_. Enabling this authentication requires that HBase is also configured to use Kerberos authentication
|
||||||
|
for RPCs (e.g `hbase.security.authentication` = `kerberos`).
|
||||||
|
|
||||||
|
[source,xml]
|
||||||
|
----
|
||||||
|
<property>
|
||||||
|
<name>hbase.security.authentication.ui</name>
|
||||||
|
<value>kerberos</value>
|
||||||
|
<description>Controls what kind of authentication should be used for the HBase web UIs.</description>
|
||||||
|
</property>
|
||||||
|
<property>
|
||||||
|
<name>hbase.security.authentication</name>
|
||||||
|
<value>kerberos</value>
|
||||||
|
<description>The Kerberos keytab file to use for SPNEGO authentication by the web server.</description>
|
||||||
|
</property>
|
||||||
|
----
|
||||||
|
|
||||||
|
A number of properties exist to configure SPNEGO authentication for the web server:
|
||||||
|
|
||||||
|
[source,xml]
|
||||||
|
----
|
||||||
|
<property>
|
||||||
|
<name>hbase.security.authentication.spnego.kerberos.principal</name>
|
||||||
|
<value>HTTP/_HOST@EXAMPLE.COM</value>
|
||||||
|
<description>Required for SPNEGO, the Kerberos principal to use for SPNEGO authentication by the
|
||||||
|
web server. The _HOST keyword will be automatically substituted with the node's
|
||||||
|
hostname.</description>
|
||||||
|
</property>
|
||||||
|
<property>
|
||||||
|
<name>hbase.security.authentication.spnego.kerberos.keytab</name>
|
||||||
|
<value>/etc/security/keytabs/spnego.service.keytab</value>
|
||||||
|
<description>Required for SPNEGO, the Kerberos keytab file to use for SPNEGO authentication by the
|
||||||
|
web server.</description>
|
||||||
|
</property>
|
||||||
|
<property>
|
||||||
|
<name>hbase.security.authentication.spnego.kerberos.name.rules</name>
|
||||||
|
<value></value>
|
||||||
|
<description>Optional, Hadoop-style `auth_to_local` rules which will be parsed and used in the
|
||||||
|
handling of Kerberos principals</description>
|
||||||
|
</property>
|
||||||
|
<property>
|
||||||
|
<name>hbase.security.authentication.signature.secret.file</name>
|
||||||
|
<value></value>
|
||||||
|
<description>Optional, a file whose contents will be used as a secret to sign the HTTP cookies
|
||||||
|
as a part of the SPNEGO authentication handshake. If this is not provided, Java's `Random` library
|
||||||
|
will be used for the secret.</description>
|
||||||
|
</property>
|
||||||
|
----
|
||||||
|
|
||||||
[[hbase.secure.configuration]]
|
[[hbase.secure.configuration]]
|
||||||
== Secure Client Access to Apache HBase
|
== Secure Client Access to Apache HBase
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue