You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by te...@apache.org on 2016/06/15 16:26:52 UTC

hbase git commit: HBASE-5291 Add Kerberos HTTP SPNEGO authentication support to HBase web consoles (Josh Elser)

Repository: hbase
Updated Branches:
  refs/heads/master 6c60bc9f6 -> ae5fe1e61


HBASE-5291 Add Kerberos HTTP SPNEGO authentication support to HBase web consoles (Josh Elser)


Project: http://git-wip-us.apache.org/repos/asf/hbase/repo
Commit: http://git-wip-us.apache.org/repos/asf/hbase/commit/ae5fe1e6
Tree: http://git-wip-us.apache.org/repos/asf/hbase/tree/ae5fe1e6
Diff: http://git-wip-us.apache.org/repos/asf/hbase/diff/ae5fe1e6

Branch: refs/heads/master
Commit: ae5fe1e61623dd36d33cd7abc94ca5e8229a4672
Parents: 6c60bc9
Author: tedyu <yu...@gmail.com>
Authored: Wed Jun 15 09:26:44 2016 -0700
Committer: tedyu <yu...@gmail.com>
Committed: Wed Jun 15 09:26:44 2016 -0700

----------------------------------------------------------------------
 hbase-server/pom.xml                            |  22 ++
 .../apache/hadoop/hbase/http/HttpServer.java    |  93 ++++++-
 .../apache/hadoop/hbase/http/InfoServer.java    |   9 +
 .../hbase/regionserver/HRegionServer.java       |   1 +
 .../hbase/http/HttpServerFunctionalTest.java    |  43 ++++
 .../hadoop/hbase/http/TestSpnegoHttpServer.java | 258 +++++++++++++++++++
 pom.xml                                         |  17 ++
 src/main/asciidoc/_chapters/security.adoc       |  53 ++++
 8 files changed, 486 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/hbase-server/pom.xml
----------------------------------------------------------------------
diff --git a/hbase-server/pom.xml b/hbase-server/pom.xml
index 9532d1e..8d1527f 100644
--- a/hbase-server/pom.xml
+++ b/hbase-server/pom.xml
@@ -572,6 +572,28 @@
         </exclusion>
       </exclusions>
     </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>
   <profiles>
     <!-- Needs to make the profile in apache parent pom -->

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/hbase-server/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java
index 667e597..e8f875e 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java
@@ -27,6 +27,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -102,11 +103,30 @@ import com.sun.jersey.spi.container.servlet.ServletContainer;
 @InterfaceStability.Evolving
 public class HttpServer implements FilterContainer {
   private static final Log LOG = LogFactory.getLog(HttpServer.class);
+  private static final String EMPTY_STRING = "";
 
   static final String FILTER_INITIALIZERS_PROPERTY
       = "hbase.http.filter.initializers";
   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
   // gets stored.
   public static final String CONF_CONTEXT_ATTRIBUTE = "hbase.conf";
@@ -175,6 +195,9 @@ public class HttpServer implements FilterContainer {
     // The -keypass option in keytool
     private String keyPassword;
 
+    private String kerberosNameRulesKey;
+    private String signatureSecretFileKey;
+
     @Deprecated
     private String name;
     @Deprecated
@@ -302,6 +325,16 @@ public class HttpServer implements FilterContainer {
       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) {
         this.appDir = appDir;
         return this;
@@ -344,7 +377,8 @@ public class HttpServer implements FilterContainer {
       HttpServer server = new HttpServer(this);
 
       if (this.securityEnabled) {
-        server.initSpnego(conf, hostName, usernameConfKey, keytabConfKey);
+        server.initSpnego(conf, hostName, usernameConfKey, keytabConfKey, kerberosNameRulesKey,
+            signatureSecretFileKey);
       }
 
       if (connector != null) {
@@ -927,21 +961,60 @@ public class HttpServer implements FilterContainer {
   }
 
   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>();
-    String principalInConf = conf.get(usernameConfKey);
-    if (principalInConf != null && !principalInConf.isEmpty()) {
-      params.put("kerberos.principal", SecurityUtil.getServerPrincipal(
+    String principalInConf = getOrEmptyString(conf, usernameConfKey);
+    if (!principalInConf.isEmpty()) {
+      params.put(HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_SUFFIX, SecurityUtil.getServerPrincipal(
           principalInConf, hostName));
     }
-    String httpKeytab = conf.get(keytabConfKey);
-    if (httpKeytab != null && !httpKeytab.isEmpty()) {
-      params.put("kerberos.keytab", httpKeytab);
+    String httpKeytab = getOrEmptyString(conf, keytabConfKey);
+    if (!httpKeytab.isEmpty()) {
+      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");
 
-    defineFilter(webAppContext, SPNEGO_FILTER,
-                 AuthenticationFilter.class.getName(), params, null);
+    // Verify that the required options were provided
+    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;
   }
 
   /**

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/hbase-server/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java
index 5ff6370..0f6c3dd 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java
@@ -75,6 +75,15 @@ public class InfoServer {
         HBaseConfiguration.getPassword(c, "ssl.server.truststore.password", null),
         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();
   }
 

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java
index b97c23f..45a1095 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java
@@ -104,6 +104,7 @@ import org.apache.hadoop.hbase.exceptions.UnknownProtocolException;
 import org.apache.hadoop.hbase.executor.ExecutorService;
 import org.apache.hadoop.hbase.executor.ExecutorType;
 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.io.hfile.CacheConfig;
 import org.apache.hadoop.hbase.io.hfile.HFile;

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/hbase-server/src/test/java/org/apache/hadoop/hbase/http/HttpServerFunctionalTest.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/HttpServerFunctionalTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/HttpServerFunctionalTest.java
index 5c832be..7e773b7 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/HttpServerFunctionalTest.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/HttpServerFunctionalTest.java
@@ -27,6 +27,7 @@ import org.apache.hadoop.hbase.http.HttpServer.Builder;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.ServerSocket;
 import java.net.URI;
 import java.net.URL;
 import java.net.MalformedURLException;
@@ -94,6 +95,15 @@ public class HttpServerFunctionalTest extends Assert {
     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
    * fail if the directory cannot be created.
@@ -226,4 +236,37 @@ public class HttpServerFunctionalTest extends Assert {
     }
     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();
+      }
+    }
+  }
 }

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/hbase-server/src/test/java/org/apache/hadoop/hbase/http/TestSpnegoHttpServer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/TestSpnegoHttpServer.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/TestSpnegoHttpServer.java
new file mode 100644
index 0000000..a84895f
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/TestSpnegoHttpServer.java
@@ -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();
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 93b7a0e..b34401d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1231,6 +1231,7 @@
     <!-- Do not use versions earlier than 3.2.2 due to a security vulnerability -->
     <collections.version>3.2.2</collections.version>
     <httpclient.version>4.3.6</httpclient.version>
+    <httpcore.version>4.4.4</httpcore.version>
     <metrics-core.version>3.1.2</metrics-core.version>
     <guava.version>12.0.1</guava.version>
     <jsr305.version>1.3.9</jsr305.version>
@@ -1260,6 +1261,7 @@
     <jcodings.version>1.0.8</jcodings.version>
     <spy.version>2.11.6</spy.version>
     <bouncycastle.version>1.46</bouncycastle.version>
+    <kerby.version>1.0.0-RC2</kerby.version>
     <!-- Plugin Dependencies -->
     <maven.assembly.version>2.4</maven.assembly.version>
     <maven.antrun.version>1.8</maven.antrun.version>
@@ -1525,6 +1527,11 @@
         <version>${httpclient.version}</version>
       </dependency>
       <dependency>
+        <groupId>org.apache.httpcomponents</groupId>
+        <artifactId>httpcore</artifactId>
+        <version>${httpcore.version}</version>
+      </dependency>
+      <dependency>
         <groupId>commons-cli</groupId>
         <artifactId>commons-cli</artifactId>
         <version>${commons-cli.version}</version>
@@ -1806,6 +1813,16 @@
        <version>${bouncycastle.version}</version>
        <scope>test</scope>
      </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>
   </dependencyManagement>
   <!-- Dependencies needed by subprojects -->

http://git-wip-us.apache.org/repos/asf/hbase/blob/ae5fe1e6/src/main/asciidoc/_chapters/security.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/_chapters/security.adoc b/src/main/asciidoc/_chapters/security.adoc
index 0d1407a..85e503c 100644
--- a/src/main/asciidoc/_chapters/security.adoc
+++ b/src/main/asciidoc/_chapters/security.adoc
@@ -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.
 ====
 
+[[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]]
 == Secure Client Access to Apache HBase