From adcf18eef4a6d556b80d1d4c074ea35a5b18c9cf Mon Sep 17 00:00:00 2001 From: Kevin Risden Date: Thu, 14 Dec 2017 10:32:00 -0600 Subject: [PATCH] HBase Thrift should use a SPNEGO HTTP/hostname principal for checking HTTP Kerberos authentication Return 401 sooner when AUTHORIZATION header is missing HBase Thrift server was checking for the AUTHORIZATION header and assuming it was always present even when it was the first request. Many clients will not send the AUTHORIZATION header until a 401 is received. HBase Thrift in the case of no header was throwing multiple exceptions and filling the logs with exceptions. This was fixed by checking that if the AUTHORIZATION header is empty then return a 401 immediately if security is enabled. --- .../hadoop/hbase/thrift/ThriftHttpServlet.java | 60 ++++++++++++++++------ .../hadoop/hbase/thrift/ThriftServerRunner.java | 53 +++++++++++++------ 2 files changed, 80 insertions(+), 33 deletions(-) mode change 100644 => 100755 hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftHttpServlet.java mode change 100644 => 100755 hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServerRunner.java diff --git a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftHttpServlet.java b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftHttpServlet.java old mode 100644 new mode 100755 index 4f55a01a08..6c0cfc90af --- a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftHttpServlet.java +++ b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftHttpServlet.java @@ -31,6 +31,7 @@ import org.apache.hadoop.hbase.util.Base64; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authorize.AuthorizationException; import org.apache.hadoop.security.authorize.ProxyUsers; +import org.apache.http.HttpHeaders; import org.apache.thrift.TProcessor; import org.apache.thrift.protocol.TProtocolFactory; import org.apache.thrift.server.TServlet; @@ -52,7 +53,8 @@ import org.slf4j.LoggerFactory; public class ThriftHttpServlet extends TServlet { private static final long serialVersionUID = 1L; private static final Logger LOG = LoggerFactory.getLogger(ThriftHttpServlet.class.getName()); - private transient final UserGroupInformation realUser; + private transient final UserGroupInformation serviceUGI; + private transient final UserGroupInformation httpUGI; private transient final Configuration conf; private final boolean securityEnabled; private final boolean doAsEnabled; @@ -60,15 +62,14 @@ public class ThriftHttpServlet extends TServlet { private String outToken; // HTTP Header related constants. - public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; - public static final String AUTHORIZATION = "Authorization"; public static final String NEGOTIATE = "Negotiate"; public ThriftHttpServlet(TProcessor processor, TProtocolFactory protocolFactory, - UserGroupInformation realUser, Configuration conf, ThriftServerRunner.HBaseHandler - hbaseHandler, boolean securityEnabled, boolean doAsEnabled) { + UserGroupInformation serviceUGI, UserGroupInformation httpUGI, Configuration conf, + ThriftServerRunner.HBaseHandler hbaseHandler, boolean securityEnabled, boolean doAsEnabled) { super(processor, protocolFactory); - this.realUser = realUser; + this.serviceUGI = serviceUGI; + this.httpUGI = httpUGI; this.conf = conf; this.hbaseHandler = hbaseHandler; this.securityEnabled = securityEnabled; @@ -80,25 +81,38 @@ public class ThriftHttpServlet extends TServlet { throws ServletException, IOException { String effectiveUser = request.getRemoteUser(); if (securityEnabled) { + /* + Check that the AUTHORIZATION header has any content. If it does not then return a 401 + requesting AUTHORIZATION header to be sent. This is typical where the first request doesn't + send the AUTHORIZATION header initially. + */ + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authHeader == null || authHeader.isEmpty()) { + // Send a 401 to the client + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + try { // As Thrift HTTP transport doesn't support SPNEGO yet (THRIFT-889), // Kerberos authentication is being done at servlet level. effectiveUser = doKerberosAuth(request); // It is standard for client applications expect this header. // Please see http://tools.ietf.org/html/rfc4559 for more details. - response.addHeader(WWW_AUTHENTICATE, NEGOTIATE + " " + outToken); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE + " " + outToken); } catch (HttpAuthenticationException e) { LOG.error("Kerberos Authentication failed", e); // Send a 401 to the client - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.addHeader(WWW_AUTHENTICATE, NEGOTIATE); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE); response.getWriter().println("Authentication Error: " + e.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } } String doAsUserFromQuery = request.getHeader("doAs"); if(effectiveUser == null) { - effectiveUser = realUser.getShortUserName(); + effectiveUser = serviceUGI.getShortUserName(); } if (doAsUserFromQuery != null) { if (!doAsEnabled) { @@ -112,7 +126,7 @@ public class ThriftHttpServlet extends TServlet { remoteUser); // validate the proxy user authorization try { - ProxyUsers.authorize(ugi, request.getRemoteAddr(), conf); + ProxyUsers.authorize(ugi, request.getRemoteAddr()); } catch (AuthorizationException e) { throw new ServletException(e.getMessage()); } @@ -129,18 +143,32 @@ public class ThriftHttpServlet extends TServlet { */ private String doKerberosAuth(HttpServletRequest request) throws HttpAuthenticationException { - HttpKerberosServerAction action = new HttpKerberosServerAction(request, realUser); + // Try authenticating with the HTTP/_HOST principal + HttpKerberosServerAction action; + String principal; + if (httpUGI != null) { + try { + action = new HttpKerberosServerAction(request, httpUGI); + principal = httpUGI.doAs(action); + outToken = action.outToken; + return principal; + } catch (Exception e) { + LOG.info("Failed to authenticate with HTTP/_HOST kerberos principal, " + + "trying with hbase/_HOST kerberos principal"); + } + } + // Now try with hbase/_HOST principal try { - String principal = realUser.doAs(action); + action = new HttpKerberosServerAction(request, serviceUGI); + principal = serviceUGI.doAs(action); outToken = action.outToken; return principal; } catch (Exception e) { - LOG.error("Failed to perform authentication"); + LOG.error("Failed to authenticate with hbase/_HOST kerberos principal"); throw new HttpAuthenticationException(e); } } - private static class HttpKerberosServerAction implements PrivilegedExceptionAction { HttpServletRequest request; UserGroupInformation serviceUGI; @@ -206,7 +234,7 @@ public class ThriftHttpServlet extends TServlet { */ private String getAuthHeader(HttpServletRequest request) throws HttpAuthenticationException { - String authHeader = request.getHeader(AUTHORIZATION); + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); // Each http request must have an Authorization header if (authHeader == null || authHeader.isEmpty()) { throw new HttpAuthenticationException("Authorization header received " + diff --git a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServerRunner.java b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServerRunner.java old mode 100644 new mode 100755 index 3ef96f65f5..a54e146f05 --- a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServerRunner.java +++ b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/ThriftServerRunner.java @@ -178,6 +178,15 @@ public class ThriftServerRunner implements Runnable { static final String THRIFT_SSL_EXCLUDE_PROTOCOLS = "hbase.thrift.ssl.exclude.protocols"; static final String THRIFT_SSL_INCLUDE_PROTOCOLS = "hbase.thrift.ssl.include.protocols"; + static final String THRIFT_SUPPORT_PROXYUSER = "hbase.thrift.support.proxyuser"; + + static final String THRIFT_DNS_INTERFACE = "hbase.thrift.dns.interface"; + static final String THRIFT_DNS_NAMESERVER = "hbase.thrift.dns.nameserver"; + static final String THRIFT_KEYTAB_FILE = "hbase.thrift.keytab.file"; + static final String THRIFT_KERBEROS_PRINCIPAL = "hbase.thrift.kerberos.principal"; + static final String THRIFT_SPNEGO_KEYTAB_FILE = "hbase.thrift.spnego.keytab.file"; + static final String THRIFT_SPNEGO_PRINCIPAL = "hbase.thrift.spnego.principal"; + /** * Amount of time in milliseconds before a server thread will timeout * waiting for client to send data on a connected socket. Currently, @@ -203,7 +212,7 @@ public class ThriftServerRunner implements Runnable { private static final String DEFAULT_BIND_ADDR = "0.0.0.0"; public static final int DEFAULT_LISTEN_PORT = 9090; public static final int HREGION_VERSION = 1; - static final String THRIFT_SUPPORT_PROXYUSER = "hbase.thrift.support.proxyuser"; + private final int listenPort; private Configuration conf; @@ -212,7 +221,8 @@ public class ThriftServerRunner implements Runnable { private final Hbase.Iface handler; private final ThriftMetrics metrics; private final HBaseHandler hbaseHandler; - private final UserGroupInformation realUser; + private final UserGroupInformation serviceUGI; + private final UserGroupInformation httpUGI; private SaslUtil.QualityOfProtection qop; private String host; @@ -227,8 +237,7 @@ public class ThriftServerRunner implements Runnable { HS_HA("hsha", true, THsHaServer.class, true), NONBLOCKING("nonblocking", true, TNonblockingServer.class, true), THREAD_POOL("threadpool", false, TBoundedThreadPoolServer.class, true), - THREADED_SELECTOR( - "threadedselector", true, TThreadedSelectorServer.class, true); + THREADED_SELECTOR("threadedselector", true, TThreadedSelectorServer.class, true); public static final ImplType DEFAULT = THREAD_POOL; @@ -246,8 +255,7 @@ public class ThriftServerRunner implements Runnable { } /** - * @return -option so we can get the list of options from - * {@link #values()} + * @return -option */ @Override public String toString() { @@ -317,7 +325,6 @@ public class ThriftServerRunner implements Runnable { } return l; } - } public ThriftServerRunner(Configuration conf) throws IOException { @@ -327,11 +334,24 @@ public class ThriftServerRunner implements Runnable { && userProvider.isHBaseSecurityEnabled(); if (securityEnabled) { host = Strings.domainNamePointerToHostName(DNS.getDefaultHost( - conf.get("hbase.thrift.dns.interface", "default"), - conf.get("hbase.thrift.dns.nameserver", "default"))); - userProvider.login("hbase.thrift.keytab.file", - "hbase.thrift.kerberos.principal", host); + conf.get(THRIFT_DNS_INTERFACE, "default"), + conf.get(THRIFT_DNS_NAMESERVER, "default"))); + userProvider.login(THRIFT_KEYTAB_FILE, THRIFT_KERBEROS_PRINCIPAL, host); } + this.serviceUGI = userProvider.getCurrent().getUGI(); + + UserProvider httpUserProvider = UserProvider.instantiate(conf); + // login the server principal (if using secure Hadoop) + boolean httpEnabled = conf.getBoolean(USE_HTTP_CONF_KEY, false); + boolean httpSecurityEnabled = userProvider.isHadoopSecurityEnabled() + && userProvider.isHBaseSecurityEnabled(); + if (httpEnabled && httpSecurityEnabled) { + httpUserProvider.login(THRIFT_SPNEGO_KEYTAB_FILE, THRIFT_SPNEGO_PRINCIPAL, host); + this.httpUGI = httpUserProvider.getCurrent().getUGI(); + } else { + this.httpUGI = null; + } + this.conf = HBaseConfiguration.create(conf); this.listenPort = conf.getInt(PORT_CONF_KEY, DEFAULT_LISTEN_PORT); this.metrics = new ThriftMetrics(conf, ThriftMetrics.ThriftServerType.ONE); @@ -340,7 +360,7 @@ public class ThriftServerRunner implements Runnable { this.hbaseHandler.initMetrics(metrics); this.handler = HbaseHandlerMetricsProxy.newInstance( hbaseHandler, metrics, conf); - this.realUser = userProvider.getCurrent().getUGI(); + String strQop = conf.get(THRIFT_QOP_KEY); if (strQop != null) { this.qop = SaslUtil.getQop(strQop); @@ -384,7 +404,7 @@ public class ThriftServerRunner implements Runnable { */ @Override public void run() { - realUser.doAs(new PrivilegedAction() { + serviceUGI.doAs(new PrivilegedAction() { @Override public Object run() { try { @@ -430,8 +450,8 @@ public class ThriftServerRunner implements Runnable { private void setupHTTPServer() throws IOException { TProtocolFactory protocolFactory = new TBinaryProtocol.Factory(); TProcessor processor = new Hbase.Processor<>(handler); - TServlet thriftHttpServlet = new ThriftHttpServlet(processor, protocolFactory, realUser, - conf, hbaseHandler, securityEnabled, doAsEnabled); + TServlet thriftHttpServlet = new ThriftHttpServlet(processor, protocolFactory, serviceUGI, + httpUGI, conf, hbaseHandler, securityEnabled, doAsEnabled); // Set the default max thread number to 100 to limit // the number of concurrent requests so that Thrfit HTTP server doesn't OOM easily. @@ -541,8 +561,7 @@ public class ThriftServerRunner implements Runnable { transportFactory = new TTransportFactory(); } else { // Extract the name from the principal - String name = SecurityUtil.getUserFromPrincipal( - conf.get("hbase.thrift.kerberos.principal")); + String name = SecurityUtil.getUserFromPrincipal(conf.get(THRIFT_KERBEROS_PRINCIPAL)); Map saslProperties = SaslUtil.initSaslProperties(qop.name()); TSaslServerTransport.Factory saslFactory = new TSaslServerTransport.Factory(); saslFactory.addServerDefinition("GSSAPI", name, host, saslProperties, -- 2.16.0