HADOOP-6584. Provide Kerberized SSL encryption for webservices.
git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@960137 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
05b1f87909
commit
4b34109a72
|
@ -13,6 +13,9 @@ Trunk (unreleased changes)
|
||||||
they can be used for authorization (Kan Zhang and Jitendra Pandey
|
they can be used for authorization (Kan Zhang and Jitendra Pandey
|
||||||
via jghoman)
|
via jghoman)
|
||||||
|
|
||||||
|
HADOOP-6584. Provide Kerberized SSL encryption for webservices.
|
||||||
|
(jghoman and Kan Zhang via jghoman)
|
||||||
|
|
||||||
IMPROVEMENTS
|
IMPROVEMENTS
|
||||||
|
|
||||||
HADOOP-6644. util.Shell getGROUPS_FOR_USER_COMMAND method name
|
HADOOP-6644. util.Shell getGROUPS_FOR_USER_COMMAND method name
|
||||||
|
|
|
@ -46,6 +46,8 @@ import org.apache.commons.logging.LogFactory;
|
||||||
import org.apache.hadoop.conf.Configuration;
|
import org.apache.hadoop.conf.Configuration;
|
||||||
import org.apache.hadoop.log.LogLevel;
|
import org.apache.hadoop.log.LogLevel;
|
||||||
import org.apache.hadoop.metrics.MetricsServlet;
|
import org.apache.hadoop.metrics.MetricsServlet;
|
||||||
|
import org.apache.hadoop.security.Krb5AndCertsSslSocketConnector;
|
||||||
|
import org.apache.hadoop.security.Krb5AndCertsSslSocketConnector.MODE;
|
||||||
import org.apache.hadoop.security.UserGroupInformation;
|
import org.apache.hadoop.security.UserGroupInformation;
|
||||||
import org.apache.hadoop.security.authorize.AccessControlList;
|
import org.apache.hadoop.security.authorize.AccessControlList;
|
||||||
import org.apache.hadoop.util.ReflectionUtils;
|
import org.apache.hadoop.util.ReflectionUtils;
|
||||||
|
@ -163,6 +165,10 @@ public class HttpServer implements FilterContainer {
|
||||||
|
|
||||||
addDefaultApps(contexts, appDir, conf);
|
addDefaultApps(contexts, appDir, conf);
|
||||||
|
|
||||||
|
defineFilter(webAppContext, "krb5Filter",
|
||||||
|
Krb5AndCertsSslSocketConnector.Krb5SslFilter.class.getName(),
|
||||||
|
null, null);
|
||||||
|
|
||||||
addGlobalFilter("safety", QuotingInputFilter.class.getName(), null);
|
addGlobalFilter("safety", QuotingInputFilter.class.getName(), null);
|
||||||
final FilterInitializer[] initializers = getFilterInitializers(conf);
|
final FilterInitializer[] initializers = getFilterInitializers(conf);
|
||||||
if (initializers != null) {
|
if (initializers != null) {
|
||||||
|
@ -290,7 +296,7 @@ public class HttpServer implements FilterContainer {
|
||||||
*/
|
*/
|
||||||
public void addServlet(String name, String pathSpec,
|
public void addServlet(String name, String pathSpec,
|
||||||
Class<? extends HttpServlet> clazz) {
|
Class<? extends HttpServlet> clazz) {
|
||||||
addInternalServlet(name, pathSpec, clazz);
|
addInternalServlet(name, pathSpec, clazz, false);
|
||||||
addFilterPathMapping(pathSpec, webAppContext);
|
addFilterPathMapping(pathSpec, webAppContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,11 +312,38 @@ public class HttpServer implements FilterContainer {
|
||||||
*/
|
*/
|
||||||
public void addInternalServlet(String name, String pathSpec,
|
public void addInternalServlet(String name, String pathSpec,
|
||||||
Class<? extends HttpServlet> clazz) {
|
Class<? extends HttpServlet> clazz) {
|
||||||
|
addInternalServlet(name, pathSpec, clazz, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an internal servlet in the server, specifying whether or not to
|
||||||
|
* protect with Kerberos authentication.
|
||||||
|
* Note: This method is to be used for adding servlets that facilitate
|
||||||
|
* internal communication and not for user facing functionality. For
|
||||||
|
* servlets added using this method, filters (except internal Kerberized
|
||||||
|
* filters) are not enabled.
|
||||||
|
*
|
||||||
|
* @param name The name of the servlet (can be passed as null)
|
||||||
|
* @param pathSpec The path spec for the servlet
|
||||||
|
* @param clazz The servlet class
|
||||||
|
*/
|
||||||
|
public void addInternalServlet(String name, String pathSpec,
|
||||||
|
Class<? extends HttpServlet> clazz, boolean requireAuth) {
|
||||||
ServletHolder holder = new ServletHolder(clazz);
|
ServletHolder holder = new ServletHolder(clazz);
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
holder.setName(name);
|
holder.setName(name);
|
||||||
}
|
}
|
||||||
webAppContext.addServlet(holder, pathSpec);
|
webAppContext.addServlet(holder, pathSpec);
|
||||||
|
|
||||||
|
if(requireAuth && UserGroupInformation.isSecurityEnabled()) {
|
||||||
|
LOG.info("Adding Kerberos filter to " + name);
|
||||||
|
ServletHandler handler = webAppContext.getServletHandler();
|
||||||
|
FilterMapping fmap = new FilterMapping();
|
||||||
|
fmap.setPathSpec(pathSpec);
|
||||||
|
fmap.setFilterName("krb5Filter");
|
||||||
|
fmap.setDispatches(Handler.ALL);
|
||||||
|
handler.addFilterMapping(fmap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/** {@inheritDoc} */
|
||||||
|
@ -451,10 +484,22 @@ public class HttpServer implements FilterContainer {
|
||||||
*/
|
*/
|
||||||
public void addSslListener(InetSocketAddress addr, Configuration sslConf,
|
public void addSslListener(InetSocketAddress addr, Configuration sslConf,
|
||||||
boolean needClientAuth) throws IOException {
|
boolean needClientAuth) throws IOException {
|
||||||
|
addSslListener(addr, sslConf, needClientAuth, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure an ssl listener on the server.
|
||||||
|
* @param addr address to listen on
|
||||||
|
* @param sslConf conf to retrieve ssl options
|
||||||
|
* @param needCertsAuth whether x509 certificate authentication is required
|
||||||
|
* @param needKrbAuth whether to allow kerberos auth
|
||||||
|
*/
|
||||||
|
public void addSslListener(InetSocketAddress addr, Configuration sslConf,
|
||||||
|
boolean needCertsAuth, boolean needKrbAuth) throws IOException {
|
||||||
if (webServer.isStarted()) {
|
if (webServer.isStarted()) {
|
||||||
throw new IOException("Failed to add ssl listener");
|
throw new IOException("Failed to add ssl listener");
|
||||||
}
|
}
|
||||||
if (needClientAuth) {
|
if (needCertsAuth) {
|
||||||
// setting up SSL truststore for authenticating clients
|
// setting up SSL truststore for authenticating clients
|
||||||
System.setProperty("javax.net.ssl.trustStore", sslConf.get(
|
System.setProperty("javax.net.ssl.trustStore", sslConf.get(
|
||||||
"ssl.server.truststore.location", ""));
|
"ssl.server.truststore.location", ""));
|
||||||
|
@ -463,14 +508,22 @@ public class HttpServer implements FilterContainer {
|
||||||
System.setProperty("javax.net.ssl.trustStoreType", sslConf.get(
|
System.setProperty("javax.net.ssl.trustStoreType", sslConf.get(
|
||||||
"ssl.server.truststore.type", "jks"));
|
"ssl.server.truststore.type", "jks"));
|
||||||
}
|
}
|
||||||
SslSocketConnector sslListener = new SslSocketConnector();
|
Krb5AndCertsSslSocketConnector.MODE mode;
|
||||||
|
if(needCertsAuth && needKrbAuth)
|
||||||
|
mode = MODE.BOTH;
|
||||||
|
else if (!needCertsAuth && needKrbAuth)
|
||||||
|
mode = MODE.KRB;
|
||||||
|
else // Default to certificates
|
||||||
|
mode = MODE.CERTS;
|
||||||
|
|
||||||
|
SslSocketConnector sslListener = new Krb5AndCertsSslSocketConnector(mode);
|
||||||
sslListener.setHost(addr.getHostName());
|
sslListener.setHost(addr.getHostName());
|
||||||
sslListener.setPort(addr.getPort());
|
sslListener.setPort(addr.getPort());
|
||||||
sslListener.setKeystore(sslConf.get("ssl.server.keystore.location"));
|
sslListener.setKeystore(sslConf.get("ssl.server.keystore.location"));
|
||||||
sslListener.setPassword(sslConf.get("ssl.server.keystore.password", ""));
|
sslListener.setPassword(sslConf.get("ssl.server.keystore.password", ""));
|
||||||
sslListener.setKeyPassword(sslConf.get("ssl.server.keystore.keypassword", ""));
|
sslListener.setKeyPassword(sslConf.get("ssl.server.keystore.keypassword", ""));
|
||||||
sslListener.setKeystoreType(sslConf.get("ssl.server.keystore.type", "jks"));
|
sslListener.setKeystoreType(sslConf.get("ssl.server.keystore.type", "jks"));
|
||||||
sslListener.setNeedClientAuth(needClientAuth);
|
sslListener.setNeedClientAuth(needCertsAuth);
|
||||||
webServer.addConnector(sslListener);
|
webServer.addConnector(sslListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
/**
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLServerSocket;
|
||||||
|
import javax.net.ssl.SSLServerSocketFactory;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||||
|
import javax.servlet.Filter;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.mortbay.io.EndPoint;
|
||||||
|
import org.mortbay.jetty.HttpSchemes;
|
||||||
|
import org.mortbay.jetty.Request;
|
||||||
|
import org.mortbay.jetty.security.ServletSSL;
|
||||||
|
import org.mortbay.jetty.security.SslSocketConnector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend Jetty's {@link SslSocketConnector} to optionally also provide
|
||||||
|
* Kerberos5ized SSL sockets. The only change in behavior from superclass
|
||||||
|
* is that we no longer honor requests to turn off NeedAuthentication when
|
||||||
|
* running with Kerberos support.
|
||||||
|
*/
|
||||||
|
public class Krb5AndCertsSslSocketConnector extends SslSocketConnector {
|
||||||
|
public static final String[] KRB5_CIPHER_SUITES =
|
||||||
|
new String [] {"TLS_KRB5_WITH_3DES_EDE_CBC_SHA"};
|
||||||
|
static {
|
||||||
|
System.setProperty("https.cipherSuites", KRB5_CIPHER_SUITES[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Log LOG = LogFactory
|
||||||
|
.getLog(Krb5AndCertsSslSocketConnector.class);
|
||||||
|
|
||||||
|
private static final String REMOTE_PRINCIPAL = "remote_principal";
|
||||||
|
|
||||||
|
public enum MODE {KRB, CERTS, BOTH} // Support Kerberos, certificates or both?
|
||||||
|
|
||||||
|
private final boolean useKrb;
|
||||||
|
private final boolean useCerts;
|
||||||
|
|
||||||
|
public Krb5AndCertsSslSocketConnector() {
|
||||||
|
super();
|
||||||
|
useKrb = true;
|
||||||
|
useCerts = false;
|
||||||
|
|
||||||
|
setPasswords();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Krb5AndCertsSslSocketConnector(MODE mode) {
|
||||||
|
super();
|
||||||
|
useKrb = mode == MODE.KRB || mode == MODE.BOTH;
|
||||||
|
useCerts = mode == MODE.CERTS || mode == MODE.BOTH;
|
||||||
|
setPasswords();
|
||||||
|
logIfDebug("useKerb = " + useKrb + ", useCerts = " + useCerts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not using Certs, set passwords to random gibberish or else
|
||||||
|
// Jetty will actually prompt the user for some.
|
||||||
|
private void setPasswords() {
|
||||||
|
if(!useCerts) {
|
||||||
|
Random r = new Random();
|
||||||
|
System.setProperty("jetty.ssl.password", String.valueOf(r.nextLong()));
|
||||||
|
System.setProperty("jetty.ssl.keypassword", String.valueOf(r.nextLong()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SSLServerSocketFactory createFactory() throws Exception {
|
||||||
|
if(useCerts)
|
||||||
|
return super.createFactory();
|
||||||
|
|
||||||
|
SSLContext context = super.getProvider()==null
|
||||||
|
? SSLContext.getInstance(super.getProtocol())
|
||||||
|
:SSLContext.getInstance(super.getProtocol(), super.getProvider());
|
||||||
|
context.init(null, null, null);
|
||||||
|
|
||||||
|
return context.getServerSocketFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see org.mortbay.jetty.security.SslSocketConnector#newServerSocket(java.lang.String, int, int)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ServerSocket newServerSocket(String host, int port, int backlog)
|
||||||
|
throws IOException {
|
||||||
|
logIfDebug("Creating new KrbServerSocket for: " + host);
|
||||||
|
SSLServerSocket ss = null;
|
||||||
|
|
||||||
|
if(useCerts) // Get the server socket from the SSL super impl
|
||||||
|
ss = (SSLServerSocket)super.newServerSocket(host, port, backlog);
|
||||||
|
else { // Create a default server socket
|
||||||
|
try {
|
||||||
|
ss = (SSLServerSocket)(host == null
|
||||||
|
? createFactory().createServerSocket(port, backlog) :
|
||||||
|
createFactory().createServerSocket(port, backlog, InetAddress.getByName(host)));
|
||||||
|
} catch (Exception e)
|
||||||
|
{
|
||||||
|
LOG.warn("Could not create KRB5 Listener", e);
|
||||||
|
throw new IOException("Could not create KRB5 Listener: " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Kerberos ciphers to this socket server if needed.
|
||||||
|
if(useKrb) {
|
||||||
|
ss.setNeedClientAuth(true);
|
||||||
|
String [] combined;
|
||||||
|
if(useCerts) { // combine the cipher suites
|
||||||
|
String[] certs = ss.getEnabledCipherSuites();
|
||||||
|
combined = new String[certs.length + KRB5_CIPHER_SUITES.length];
|
||||||
|
System.arraycopy(certs, 0, combined, 0, certs.length);
|
||||||
|
System.arraycopy(KRB5_CIPHER_SUITES, 0, combined, certs.length, KRB5_CIPHER_SUITES.length);
|
||||||
|
} else { // Just enable Kerberos auth
|
||||||
|
combined = KRB5_CIPHER_SUITES;
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.setEnabledCipherSuites(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(EndPoint endpoint, Request request) throws IOException {
|
||||||
|
if(useKrb) { // Add Kerberos-specific info
|
||||||
|
SSLSocket sslSocket = (SSLSocket)endpoint.getTransport();
|
||||||
|
Principal remotePrincipal = sslSocket.getSession().getPeerPrincipal();
|
||||||
|
logIfDebug("Remote principal = " + remotePrincipal);
|
||||||
|
request.setScheme(HttpSchemes.HTTPS);
|
||||||
|
request.setAttribute(REMOTE_PRINCIPAL, remotePrincipal);
|
||||||
|
|
||||||
|
if(!useCerts) { // Add extra info that would have been added by super
|
||||||
|
String cipherSuite = sslSocket.getSession().getCipherSuite();
|
||||||
|
Integer keySize = Integer.valueOf(ServletSSL.deduceKeyLength(cipherSuite));;
|
||||||
|
|
||||||
|
request.setAttribute("javax.servlet.request.cipher_suite", cipherSuite);
|
||||||
|
request.setAttribute("javax.servlet.request.key_size", keySize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(useCerts) super.customize(endpoint, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logIfDebug(String s) {
|
||||||
|
if(LOG.isDebugEnabled())
|
||||||
|
LOG.debug(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter that takes the Kerberos principal identified in the
|
||||||
|
* {@link Krb5AndCertsSslSocketConnector} and provides it the to the servlet
|
||||||
|
* at runtime, setting the principal and short name.
|
||||||
|
*/
|
||||||
|
public static class Krb5SslFilter implements Filter {
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest req, ServletResponse resp,
|
||||||
|
FilterChain chain) throws IOException, ServletException {
|
||||||
|
final Principal princ =
|
||||||
|
(Principal)req.getAttribute(Krb5AndCertsSslSocketConnector.REMOTE_PRINCIPAL);
|
||||||
|
|
||||||
|
if(princ == null || !(princ instanceof KerberosPrincipal)) {
|
||||||
|
// Should never actually get here, since should be rejected at socket
|
||||||
|
// level.
|
||||||
|
LOG.warn("User not authenticated via kerberos from " + req.getRemoteAddr());
|
||||||
|
((HttpServletResponse)resp).sendError(HttpServletResponse.SC_FORBIDDEN,
|
||||||
|
"User not authenticated via Kerberos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide principal information for servlet at runtime
|
||||||
|
ServletRequest wrapper =
|
||||||
|
new HttpServletRequestWrapper((HttpServletRequest) req) {
|
||||||
|
@Override
|
||||||
|
public Principal getUserPrincipal() {
|
||||||
|
return princ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return the full name of this remote user.
|
||||||
|
* @see javax.servlet.http.HttpServletRequestWrapper#getRemoteUser()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getRemoteUser() {
|
||||||
|
return princ.getName();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chain.doFilter(wrapper, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig arg0) throws ServletException {
|
||||||
|
/* Nothing to do here */
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() { /* Nothing to do here */ }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue