Skip to content

Commit 6ce02c7

Browse files
committed
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
1 parent 485edf1 commit 6ce02c7

File tree

3 files changed

+289
-5
lines changed

3 files changed

+289
-5
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Trunk (unreleased changes)
1313
they can be used for authorization (Kan Zhang and Jitendra Pandey
1414
via jghoman)
1515

16+
HADOOP-6584. Provide Kerberized SSL encryption for webservices.
17+
(jghoman and Kan Zhang via jghoman)
18+
1619
IMPROVEMENTS
1720

1821
HADOOP-6644. util.Shell getGROUPS_FOR_USER_COMMAND method name

src/java/org/apache/hadoop/http/HttpServer.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import org.apache.hadoop.conf.Configuration;
4747
import org.apache.hadoop.log.LogLevel;
4848
import org.apache.hadoop.metrics.MetricsServlet;
49+
import org.apache.hadoop.security.Krb5AndCertsSslSocketConnector;
50+
import org.apache.hadoop.security.Krb5AndCertsSslSocketConnector.MODE;
4951
import org.apache.hadoop.security.UserGroupInformation;
5052
import org.apache.hadoop.security.authorize.AccessControlList;
5153
import org.apache.hadoop.util.ReflectionUtils;
@@ -162,7 +164,11 @@ public HttpServer(String name, String bindAddress, int port,
162164
webServer.addHandler(webAppContext);
163165

164166
addDefaultApps(contexts, appDir, conf);
165-
167+
168+
defineFilter(webAppContext, "krb5Filter",
169+
Krb5AndCertsSslSocketConnector.Krb5SslFilter.class.getName(),
170+
null, null);
171+
166172
addGlobalFilter("safety", QuotingInputFilter.class.getName(), null);
167173
final FilterInitializer[] initializers = getFilterInitializers(conf);
168174
if (initializers != null) {
@@ -290,7 +296,7 @@ public void setAttribute(String name, Object value) {
290296
*/
291297
public void addServlet(String name, String pathSpec,
292298
Class<? extends HttpServlet> clazz) {
293-
addInternalServlet(name, pathSpec, clazz);
299+
addInternalServlet(name, pathSpec, clazz, false);
294300
addFilterPathMapping(pathSpec, webAppContext);
295301
}
296302

@@ -306,11 +312,38 @@ public void addServlet(String name, String pathSpec,
306312
*/
307313
public void addInternalServlet(String name, String pathSpec,
308314
Class<? extends HttpServlet> clazz) {
315+
addInternalServlet(name, pathSpec, clazz, false);
316+
}
317+
318+
/**
319+
* Add an internal servlet in the server, specifying whether or not to
320+
* protect with Kerberos authentication.
321+
* Note: This method is to be used for adding servlets that facilitate
322+
* internal communication and not for user facing functionality. For
323+
* servlets added using this method, filters (except internal Kerberized
324+
* filters) are not enabled.
325+
*
326+
* @param name The name of the servlet (can be passed as null)
327+
* @param pathSpec The path spec for the servlet
328+
* @param clazz The servlet class
329+
*/
330+
public void addInternalServlet(String name, String pathSpec,
331+
Class<? extends HttpServlet> clazz, boolean requireAuth) {
309332
ServletHolder holder = new ServletHolder(clazz);
310333
if (name != null) {
311334
holder.setName(name);
312335
}
313336
webAppContext.addServlet(holder, pathSpec);
337+
338+
if(requireAuth && UserGroupInformation.isSecurityEnabled()) {
339+
LOG.info("Adding Kerberos filter to " + name);
340+
ServletHandler handler = webAppContext.getServletHandler();
341+
FilterMapping fmap = new FilterMapping();
342+
fmap.setPathSpec(pathSpec);
343+
fmap.setFilterName("krb5Filter");
344+
fmap.setDispatches(Handler.ALL);
345+
handler.addFilterMapping(fmap);
346+
}
314347
}
315348

316349
/** {@inheritDoc} */
@@ -451,10 +484,22 @@ public void addSslListener(InetSocketAddress addr, String keystore,
451484
*/
452485
public void addSslListener(InetSocketAddress addr, Configuration sslConf,
453486
boolean needClientAuth) throws IOException {
487+
addSslListener(addr, sslConf, needClientAuth, false);
488+
}
489+
490+
/**
491+
* Configure an ssl listener on the server.
492+
* @param addr address to listen on
493+
* @param sslConf conf to retrieve ssl options
494+
* @param needCertsAuth whether x509 certificate authentication is required
495+
* @param needKrbAuth whether to allow kerberos auth
496+
*/
497+
public void addSslListener(InetSocketAddress addr, Configuration sslConf,
498+
boolean needCertsAuth, boolean needKrbAuth) throws IOException {
454499
if (webServer.isStarted()) {
455500
throw new IOException("Failed to add ssl listener");
456501
}
457-
if (needClientAuth) {
502+
if (needCertsAuth) {
458503
// setting up SSL truststore for authenticating clients
459504
System.setProperty("javax.net.ssl.trustStore", sslConf.get(
460505
"ssl.server.truststore.location", ""));
@@ -463,14 +508,22 @@ public void addSslListener(InetSocketAddress addr, Configuration sslConf,
463508
System.setProperty("javax.net.ssl.trustStoreType", sslConf.get(
464509
"ssl.server.truststore.type", "jks"));
465510
}
466-
SslSocketConnector sslListener = new SslSocketConnector();
511+
Krb5AndCertsSslSocketConnector.MODE mode;
512+
if(needCertsAuth && needKrbAuth)
513+
mode = MODE.BOTH;
514+
else if (!needCertsAuth && needKrbAuth)
515+
mode = MODE.KRB;
516+
else // Default to certificates
517+
mode = MODE.CERTS;
518+
519+
SslSocketConnector sslListener = new Krb5AndCertsSslSocketConnector(mode);
467520
sslListener.setHost(addr.getHostName());
468521
sslListener.setPort(addr.getPort());
469522
sslListener.setKeystore(sslConf.get("ssl.server.keystore.location"));
470523
sslListener.setPassword(sslConf.get("ssl.server.keystore.password", ""));
471524
sslListener.setKeyPassword(sslConf.get("ssl.server.keystore.keypassword", ""));
472525
sslListener.setKeystoreType(sslConf.get("ssl.server.keystore.type", "jks"));
473-
sslListener.setNeedClientAuth(needClientAuth);
526+
sslListener.setNeedClientAuth(needCertsAuth);
474527
webServer.addConnector(sslListener);
475528
}
476529

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with this
4+
* work for additional information regarding copyright ownership. The ASF
5+
* licenses this file to you under the Apache License, Version 2.0 (the
6+
* "License"); you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations under
15+
* the License.
16+
*/
17+
package org.apache.hadoop.security;
18+
19+
import java.io.IOException;
20+
import java.net.InetAddress;
21+
import java.net.ServerSocket;
22+
import java.security.Principal;
23+
import java.util.Random;
24+
25+
import javax.net.ssl.SSLContext;
26+
import javax.net.ssl.SSLServerSocket;
27+
import javax.net.ssl.SSLServerSocketFactory;
28+
import javax.net.ssl.SSLSocket;
29+
import javax.security.auth.kerberos.KerberosPrincipal;
30+
import javax.servlet.Filter;
31+
import javax.servlet.FilterChain;
32+
import javax.servlet.FilterConfig;
33+
import javax.servlet.ServletException;
34+
import javax.servlet.ServletRequest;
35+
import javax.servlet.ServletResponse;
36+
import javax.servlet.http.HttpServletRequest;
37+
import javax.servlet.http.HttpServletRequestWrapper;
38+
import javax.servlet.http.HttpServletResponse;
39+
40+
import org.apache.commons.logging.Log;
41+
import org.apache.commons.logging.LogFactory;
42+
import org.mortbay.io.EndPoint;
43+
import org.mortbay.jetty.HttpSchemes;
44+
import org.mortbay.jetty.Request;
45+
import org.mortbay.jetty.security.ServletSSL;
46+
import org.mortbay.jetty.security.SslSocketConnector;
47+
48+
/**
49+
* Extend Jetty's {@link SslSocketConnector} to optionally also provide
50+
* Kerberos5ized SSL sockets. The only change in behavior from superclass
51+
* is that we no longer honor requests to turn off NeedAuthentication when
52+
* running with Kerberos support.
53+
*/
54+
public class Krb5AndCertsSslSocketConnector extends SslSocketConnector {
55+
public static final String[] KRB5_CIPHER_SUITES =
56+
new String [] {"TLS_KRB5_WITH_3DES_EDE_CBC_SHA"};
57+
static {
58+
System.setProperty("https.cipherSuites", KRB5_CIPHER_SUITES[0]);
59+
}
60+
61+
private static final Log LOG = LogFactory
62+
.getLog(Krb5AndCertsSslSocketConnector.class);
63+
64+
private static final String REMOTE_PRINCIPAL = "remote_principal";
65+
66+
public enum MODE {KRB, CERTS, BOTH} // Support Kerberos, certificates or both?
67+
68+
private final boolean useKrb;
69+
private final boolean useCerts;
70+
71+
public Krb5AndCertsSslSocketConnector() {
72+
super();
73+
useKrb = true;
74+
useCerts = false;
75+
76+
setPasswords();
77+
}
78+
79+
public Krb5AndCertsSslSocketConnector(MODE mode) {
80+
super();
81+
useKrb = mode == MODE.KRB || mode == MODE.BOTH;
82+
useCerts = mode == MODE.CERTS || mode == MODE.BOTH;
83+
setPasswords();
84+
logIfDebug("useKerb = " + useKrb + ", useCerts = " + useCerts);
85+
}
86+
87+
// If not using Certs, set passwords to random gibberish or else
88+
// Jetty will actually prompt the user for some.
89+
private void setPasswords() {
90+
if(!useCerts) {
91+
Random r = new Random();
92+
System.setProperty("jetty.ssl.password", String.valueOf(r.nextLong()));
93+
System.setProperty("jetty.ssl.keypassword", String.valueOf(r.nextLong()));
94+
}
95+
}
96+
97+
@Override
98+
protected SSLServerSocketFactory createFactory() throws Exception {
99+
if(useCerts)
100+
return super.createFactory();
101+
102+
SSLContext context = super.getProvider()==null
103+
? SSLContext.getInstance(super.getProtocol())
104+
:SSLContext.getInstance(super.getProtocol(), super.getProvider());
105+
context.init(null, null, null);
106+
107+
return context.getServerSocketFactory();
108+
}
109+
110+
/* (non-Javadoc)
111+
* @see org.mortbay.jetty.security.SslSocketConnector#newServerSocket(java.lang.String, int, int)
112+
*/
113+
@Override
114+
protected ServerSocket newServerSocket(String host, int port, int backlog)
115+
throws IOException {
116+
logIfDebug("Creating new KrbServerSocket for: " + host);
117+
SSLServerSocket ss = null;
118+
119+
if(useCerts) // Get the server socket from the SSL super impl
120+
ss = (SSLServerSocket)super.newServerSocket(host, port, backlog);
121+
else { // Create a default server socket
122+
try {
123+
ss = (SSLServerSocket)(host == null
124+
? createFactory().createServerSocket(port, backlog) :
125+
createFactory().createServerSocket(port, backlog, InetAddress.getByName(host)));
126+
} catch (Exception e)
127+
{
128+
LOG.warn("Could not create KRB5 Listener", e);
129+
throw new IOException("Could not create KRB5 Listener: " + e.toString());
130+
}
131+
}
132+
133+
// Add Kerberos ciphers to this socket server if needed.
134+
if(useKrb) {
135+
ss.setNeedClientAuth(true);
136+
String [] combined;
137+
if(useCerts) { // combine the cipher suites
138+
String[] certs = ss.getEnabledCipherSuites();
139+
combined = new String[certs.length + KRB5_CIPHER_SUITES.length];
140+
System.arraycopy(certs, 0, combined, 0, certs.length);
141+
System.arraycopy(KRB5_CIPHER_SUITES, 0, combined, certs.length, KRB5_CIPHER_SUITES.length);
142+
} else { // Just enable Kerberos auth
143+
combined = KRB5_CIPHER_SUITES;
144+
}
145+
146+
ss.setEnabledCipherSuites(combined);
147+
}
148+
149+
return ss;
150+
};
151+
152+
@Override
153+
public void customize(EndPoint endpoint, Request request) throws IOException {
154+
if(useKrb) { // Add Kerberos-specific info
155+
SSLSocket sslSocket = (SSLSocket)endpoint.getTransport();
156+
Principal remotePrincipal = sslSocket.getSession().getPeerPrincipal();
157+
logIfDebug("Remote principal = " + remotePrincipal);
158+
request.setScheme(HttpSchemes.HTTPS);
159+
request.setAttribute(REMOTE_PRINCIPAL, remotePrincipal);
160+
161+
if(!useCerts) { // Add extra info that would have been added by super
162+
String cipherSuite = sslSocket.getSession().getCipherSuite();
163+
Integer keySize = Integer.valueOf(ServletSSL.deduceKeyLength(cipherSuite));;
164+
165+
request.setAttribute("javax.servlet.request.cipher_suite", cipherSuite);
166+
request.setAttribute("javax.servlet.request.key_size", keySize);
167+
}
168+
}
169+
170+
if(useCerts) super.customize(endpoint, request);
171+
}
172+
173+
private void logIfDebug(String s) {
174+
if(LOG.isDebugEnabled())
175+
LOG.debug(s);
176+
}
177+
178+
/**
179+
* Filter that takes the Kerberos principal identified in the
180+
* {@link Krb5AndCertsSslSocketConnector} and provides it the to the servlet
181+
* at runtime, setting the principal and short name.
182+
*/
183+
public static class Krb5SslFilter implements Filter {
184+
@Override
185+
public void doFilter(ServletRequest req, ServletResponse resp,
186+
FilterChain chain) throws IOException, ServletException {
187+
final Principal princ =
188+
(Principal)req.getAttribute(Krb5AndCertsSslSocketConnector.REMOTE_PRINCIPAL);
189+
190+
if(princ == null || !(princ instanceof KerberosPrincipal)) {
191+
// Should never actually get here, since should be rejected at socket
192+
// level.
193+
LOG.warn("User not authenticated via kerberos from " + req.getRemoteAddr());
194+
((HttpServletResponse)resp).sendError(HttpServletResponse.SC_FORBIDDEN,
195+
"User not authenticated via Kerberos");
196+
return;
197+
}
198+
199+
// Provide principal information for servlet at runtime
200+
ServletRequest wrapper =
201+
new HttpServletRequestWrapper((HttpServletRequest) req) {
202+
@Override
203+
public Principal getUserPrincipal() {
204+
return princ;
205+
}
206+
207+
/*
208+
* Return the full name of this remote user.
209+
* @see javax.servlet.http.HttpServletRequestWrapper#getRemoteUser()
210+
*/
211+
@Override
212+
public String getRemoteUser() {
213+
return princ.getName();
214+
}
215+
};
216+
217+
chain.doFilter(wrapper, resp);
218+
}
219+
220+
@Override
221+
public void init(FilterConfig arg0) throws ServletException {
222+
/* Nothing to do here */
223+
}
224+
225+
@Override
226+
public void destroy() { /* Nothing to do here */ }
227+
}
228+
}

0 commit comments

Comments
 (0)