001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with this
004 * work for additional information regarding copyright ownership. The ASF
005 * licenses this file to you under the Apache License, Version 2.0 (the
006 * "License"); you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations under
015 * the License.
016 */
017 package org.apache.hadoop.security;
018
019 import java.io.IOException;
020 import java.net.InetAddress;
021 import java.net.InetSocketAddress;
022 import java.net.URI;
023 import java.net.URL;
024 import java.net.UnknownHostException;
025 import java.security.AccessController;
026 import java.util.Arrays;
027 import java.util.List;
028 import java.util.ServiceLoader;
029 import java.util.Set;
030
031 import javax.security.auth.Subject;
032 import javax.security.auth.kerberos.KerberosPrincipal;
033 import javax.security.auth.kerberos.KerberosTicket;
034
035 import org.apache.commons.logging.Log;
036 import org.apache.commons.logging.LogFactory;
037 import org.apache.hadoop.classification.InterfaceAudience;
038 import org.apache.hadoop.classification.InterfaceStability;
039 import org.apache.hadoop.conf.Configuration;
040 import org.apache.hadoop.fs.CommonConfigurationKeys;
041 import org.apache.hadoop.io.Text;
042 import org.apache.hadoop.net.NetUtils;
043 import org.apache.hadoop.security.token.Token;
044 import org.apache.hadoop.security.token.TokenInfo;
045
046 import com.google.common.annotations.VisibleForTesting;
047
048 //this will need to be replaced someday when there is a suitable replacement
049 import sun.net.dns.ResolverConfiguration;
050 import sun.net.util.IPAddressUtil;
051 import sun.security.jgss.krb5.Krb5Util;
052 import sun.security.krb5.Credentials;
053 import sun.security.krb5.PrincipalName;
054
055 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
056 @InterfaceStability.Evolving
057 public class SecurityUtil {
058 public static final Log LOG = LogFactory.getLog(SecurityUtil.class);
059 public static final String HOSTNAME_PATTERN = "_HOST";
060
061 // controls whether buildTokenService will use an ip or host/ip as given
062 // by the user
063 @VisibleForTesting
064 static boolean useIpForTokenService;
065 @VisibleForTesting
066 static HostResolver hostResolver;
067
068 static {
069 boolean useIp = new Configuration().getBoolean(
070 CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP,
071 CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT);
072 setTokenServiceUseIp(useIp);
073 }
074
075 /**
076 * For use only by tests and initialization
077 */
078 @InterfaceAudience.Private
079 static void setTokenServiceUseIp(boolean flag) {
080 useIpForTokenService = flag;
081 hostResolver = !useIpForTokenService
082 ? new QualifiedHostResolver()
083 : new StandardHostResolver();
084 }
085
086 /**
087 * Find the original TGT within the current subject's credentials. Cross-realm
088 * TGT's of the form "krbtgt/[email protected]" may be present.
089 *
090 * @return The TGT from the current subject
091 * @throws IOException
092 * if TGT can't be found
093 */
094 private static KerberosTicket getTgtFromSubject() throws IOException {
095 Subject current = Subject.getSubject(AccessController.getContext());
096 if (current == null) {
097 throw new IOException(
098 "Can't get TGT from current Subject, because it is null");
099 }
100 Set<KerberosTicket> tickets = current
101 .getPrivateCredentials(KerberosTicket.class);
102 for (KerberosTicket t : tickets) {
103 if (isOriginalTGT(t))
104 return t;
105 }
106 throw new IOException("Failed to find TGT from current Subject:"+current);
107 }
108
109 /**
110 * TGS must have the server principal of the form "krbtgt/FOO@FOO".
111 * @param principal
112 * @return true or false
113 */
114 static boolean
115 isTGSPrincipal(KerberosPrincipal principal) {
116 if (principal == null)
117 return false;
118 if (principal.getName().equals("krbtgt/" + principal.getRealm() +
119 "@" + principal.getRealm())) {
120 return true;
121 }
122 return false;
123 }
124
125 /**
126 * Check whether the server principal is the TGS's principal
127 * @param ticket the original TGT (the ticket that is obtained when a
128 * kinit is done)
129 * @return true or false
130 */
131 protected static boolean isOriginalTGT(KerberosTicket ticket) {
132 return isTGSPrincipal(ticket.getServer());
133 }
134
135 /**
136 * Explicitly pull the service ticket for the specified host. This solves a
137 * problem with Java's Kerberos SSL problem where the client cannot
138 * authenticate against a cross-realm service. It is necessary for clients
139 * making kerberized https requests to call this method on the target URL
140 * to ensure that in a cross-realm environment the remote host will be
141 * successfully authenticated.
142 *
143 * This method is internal to Hadoop and should not be used by other
144 * applications. This method should not be considered stable or open:
145 * it will be removed when the Java behavior is changed.
146 *
147 * @param remoteHost Target URL the krb-https client will access
148 * @throws IOException if the service ticket cannot be retrieved
149 */
150 public static void fetchServiceTicket(URL remoteHost) throws IOException {
151 if(!UserGroupInformation.isSecurityEnabled())
152 return;
153
154 String serviceName = "host/" + remoteHost.getHost();
155 if (LOG.isDebugEnabled())
156 LOG.debug("Fetching service ticket for host at: " + serviceName);
157 Credentials serviceCred = null;
158 try {
159 PrincipalName principal = new PrincipalName(serviceName,
160 PrincipalName.KRB_NT_SRV_HST);
161 serviceCred = Credentials.acquireServiceCreds(principal
162 .toString(), Krb5Util.ticketToCreds(getTgtFromSubject()));
163 } catch (Exception e) {
164 throw new IOException("Can't get service ticket for: "
165 + serviceName, e);
166 }
167 if (serviceCred == null) {
168 throw new IOException("Can't get service ticket for " + serviceName);
169 }
170 Subject.getSubject(AccessController.getContext()).getPrivateCredentials()
171 .add(Krb5Util.credsToTicket(serviceCred));
172 }
173
174 /**
175 * Convert Kerberos principal name pattern to valid Kerberos principal
176 * names. It replaces hostname pattern with hostname, which should be
177 * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses
178 * dynamically looked-up fqdn of the current host instead.
179 *
180 * @param principalConfig
181 * the Kerberos principal name conf value to convert
182 * @param hostname
183 * the fully-qualified domain name used for substitution
184 * @return converted Kerberos principal name
185 * @throws IOException if the client address cannot be determined
186 */
187 public static String getServerPrincipal(String principalConfig,
188 String hostname) throws IOException {
189 String[] components = getComponents(principalConfig);
190 if (components == null || components.length != 3
191 || !components[1].equals(HOSTNAME_PATTERN)) {
192 return principalConfig;
193 } else {
194 return replacePattern(components, hostname);
195 }
196 }
197
198 /**
199 * Convert Kerberos principal name pattern to valid Kerberos principal names.
200 * This method is similar to {@link #getServerPrincipal(String, String)},
201 * except 1) the reverse DNS lookup from addr to hostname is done only when
202 * necessary, 2) param addr can't be null (no default behavior of using local
203 * hostname when addr is null).
204 *
205 * @param principalConfig
206 * Kerberos principal name pattern to convert
207 * @param addr
208 * InetAddress of the host used for substitution
209 * @return converted Kerberos principal name
210 * @throws IOException if the client address cannot be determined
211 */
212 public static String getServerPrincipal(String principalConfig,
213 InetAddress addr) throws IOException {
214 String[] components = getComponents(principalConfig);
215 if (components == null || components.length != 3
216 || !components[1].equals(HOSTNAME_PATTERN)) {
217 return principalConfig;
218 } else {
219 if (addr == null) {
220 throw new IOException("Can't replace " + HOSTNAME_PATTERN
221 + " pattern since client address is null");
222 }
223 return replacePattern(components, addr.getCanonicalHostName());
224 }
225 }
226
227 private static String[] getComponents(String principalConfig) {
228 if (principalConfig == null)
229 return null;
230 return principalConfig.split("[/@]");
231 }
232
233 private static String replacePattern(String[] components, String hostname)
234 throws IOException {
235 String fqdn = hostname;
236 if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) {
237 fqdn = getLocalHostName();
238 }
239 return components[0] + "/" + fqdn.toLowerCase() + "@" + components[2];
240 }
241
242 static String getLocalHostName() throws UnknownHostException {
243 return InetAddress.getLocalHost().getCanonicalHostName();
244 }
245
246 /**
247 * Login as a principal specified in config. Substitute $host in
248 * user's Kerberos principal name with a dynamically looked-up fully-qualified
249 * domain name of the current host.
250 *
251 * @param conf
252 * conf to use
253 * @param keytabFileKey
254 * the key to look for keytab file in conf
255 * @param userNameKey
256 * the key to look for user's Kerberos principal name in conf
257 * @throws IOException if login fails
258 */
259 public static void login(final Configuration conf,
260 final String keytabFileKey, final String userNameKey) throws IOException {
261 login(conf, keytabFileKey, userNameKey, getLocalHostName());
262 }
263
264 /**
265 * Login as a principal specified in config. Substitute $host in user's Kerberos principal
266 * name with hostname. If non-secure mode - return. If no keytab available -
267 * bail out with an exception
268 *
269 * @param conf
270 * conf to use
271 * @param keytabFileKey
272 * the key to look for keytab file in conf
273 * @param userNameKey
274 * the key to look for user's Kerberos principal name in conf
275 * @param hostname
276 * hostname to use for substitution
277 * @throws IOException if the config doesn't specify a keytab
278 */
279 public static void login(final Configuration conf,
280 final String keytabFileKey, final String userNameKey, String hostname)
281 throws IOException {
282
283 if(! UserGroupInformation.isSecurityEnabled())
284 return;
285
286 String keytabFilename = conf.get(keytabFileKey);
287 if (keytabFilename == null || keytabFilename.length() == 0) {
288 throw new IOException("Running in secure mode, but config doesn't have a keytab");
289 }
290
291 String principalConfig = conf.get(userNameKey, System
292 .getProperty("user.name"));
293 String principalName = SecurityUtil.getServerPrincipal(principalConfig,
294 hostname);
295 UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename);
296 }
297
298 /**
299 * create the service name for a Delegation token
300 * @param uri of the service
301 * @param defPort is used if the uri lacks a port
302 * @return the token service, or null if no authority
303 * @see #buildTokenService(InetSocketAddress)
304 */
305 public static String buildDTServiceName(URI uri, int defPort) {
306 String authority = uri.getAuthority();
307 if (authority == null) {
308 return null;
309 }
310 InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort);
311 return buildTokenService(addr).toString();
312 }
313
314 /**
315 * Get the host name from the principal name of format <service>/host@realm.
316 * @param principalName principal name of format as described above
317 * @return host name if the the string conforms to the above format, else null
318 */
319 public static String getHostFromPrincipal(String principalName) {
320 return new HadoopKerberosName(principalName).getHostName();
321 }
322
323 private static ServiceLoader<SecurityInfo> securityInfoProviders =
324 ServiceLoader.load(SecurityInfo.class);
325 private static SecurityInfo[] testProviders = new SecurityInfo[0];
326
327 /**
328 * Test setup method to register additional providers.
329 * @param providers a list of high priority providers to use
330 */
331 @InterfaceAudience.Private
332 public static void setSecurityInfoProviders(SecurityInfo... providers) {
333 testProviders = providers;
334 }
335
336 /**
337 * Look up the KerberosInfo for a given protocol. It searches all known
338 * SecurityInfo providers.
339 * @param protocol the protocol class to get the information for
340 * @param conf configuration object
341 * @return the KerberosInfo or null if it has no KerberosInfo defined
342 */
343 public static KerberosInfo
344 getKerberosInfo(Class<?> protocol, Configuration conf) {
345 synchronized (testProviders) {
346 for(SecurityInfo provider: testProviders) {
347 KerberosInfo result = provider.getKerberosInfo(protocol, conf);
348 if (result != null) {
349 return result;
350 }
351 }
352 }
353
354 synchronized (securityInfoProviders) {
355 for(SecurityInfo provider: securityInfoProviders) {
356 KerberosInfo result = provider.getKerberosInfo(protocol, conf);
357 if (result != null) {
358 return result;
359 }
360 }
361 }
362 return null;
363 }
364
365 /**
366 * Look up the TokenInfo for a given protocol. It searches all known
367 * SecurityInfo providers.
368 * @param protocol The protocol class to get the information for.
369 * @param conf Configuration object
370 * @return the TokenInfo or null if it has no KerberosInfo defined
371 */
372 public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) {
373 synchronized (testProviders) {
374 for(SecurityInfo provider: testProviders) {
375 TokenInfo result = provider.getTokenInfo(protocol, conf);
376 if (result != null) {
377 return result;
378 }
379 }
380 }
381
382 synchronized (securityInfoProviders) {
383 for(SecurityInfo provider: securityInfoProviders) {
384 TokenInfo result = provider.getTokenInfo(protocol, conf);
385 if (result != null) {
386 return result;
387 }
388 }
389 }
390
391 return null;
392 }
393
394 /**
395 * Decode the given token's service field into an InetAddress
396 * @param token from which to obtain the service
397 * @return InetAddress for the service
398 */
399 public static InetSocketAddress getTokenServiceAddr(Token<?> token) {
400 return NetUtils.createSocketAddr(token.getService().toString());
401 }
402
403 /**
404 * Set the given token's service to the format expected by the RPC client
405 * @param token a delegation token
406 * @param addr the socket for the rpc connection
407 */
408 public static void setTokenService(Token<?> token, InetSocketAddress addr) {
409 Text service = buildTokenService(addr);
410 if (token != null) {
411 token.setService(service);
412 if (LOG.isDebugEnabled()) {
413 LOG.debug("Acquired token "+token); // Token#toString() prints service
414 }
415 } else {
416 LOG.warn("Failed to get token for service "+service);
417 }
418 }
419
420 /**
421 * Construct the service key for a token
422 * @param addr InetSocketAddress of remote connection with a token
423 * @return "ip:port" or "host:port" depending on the value of
424 * hadoop.security.token.service.use_ip
425 */
426 public static Text buildTokenService(InetSocketAddress addr) {
427 String host = null;
428 if (useIpForTokenService) {
429 if (addr.isUnresolved()) { // host has no ip address
430 throw new IllegalArgumentException(
431 new UnknownHostException(addr.getHostName())
432 );
433 }
434 host = addr.getAddress().getHostAddress();
435 } else {
436 host = addr.getHostName().toLowerCase();
437 }
438 return new Text(host + ":" + addr.getPort());
439 }
440
441 /**
442 * Construct the service key for a token
443 * @param uri of remote connection with a token
444 * @return "ip:port" or "host:port" depending on the value of
445 * hadoop.security.token.service.use_ip
446 */
447 public static Text buildTokenService(URI uri) {
448 return buildTokenService(NetUtils.createSocketAddr(uri.getAuthority()));
449 }
450
451 /**
452 * Resolves a host subject to the security requirements determined by
453 * hadoop.security.token.service.use_ip.
454 *
455 * @param hostname host or ip to resolve
456 * @return a resolved host
457 * @throws UnknownHostException if the host doesn't exist
458 */
459 @InterfaceAudience.Private
460 public static
461 InetAddress getByName(String hostname) throws UnknownHostException {
462 return hostResolver.getByName(hostname);
463 }
464
465 interface HostResolver {
466 InetAddress getByName(String host) throws UnknownHostException;
467 }
468
469 /**
470 * Uses standard java host resolution
471 */
472 static class StandardHostResolver implements HostResolver {
473 public InetAddress getByName(String host) throws UnknownHostException {
474 return InetAddress.getByName(host);
475 }
476 }
477
478 /**
479 * This an alternate resolver with important properties that the standard
480 * java resolver lacks:
481 * 1) The hostname is fully qualified. This avoids security issues if not
482 * all hosts in the cluster do not share the same search domains. It
483 * also prevents other hosts from performing unnecessary dns searches.
484 * In contrast, InetAddress simply returns the host as given.
485 * 2) The InetAddress is instantiated with an exact host and IP to prevent
486 * further unnecessary lookups. InetAddress may perform an unnecessary
487 * reverse lookup for an IP.
488 * 3) A call to getHostName() will always return the qualified hostname, or
489 * more importantly, the IP if instantiated with an IP. This avoids
490 * unnecessary dns timeouts if the host is not resolvable.
491 * 4) Point 3 also ensures that if the host is re-resolved, ex. during a
492 * connection re-attempt, that a reverse lookup to host and forward
493 * lookup to IP is not performed since the reverse/forward mappings may
494 * not always return the same IP. If the client initiated a connection
495 * with an IP, then that IP is all that should ever be contacted.
496 *
497 * NOTE: this resolver is only used if:
498 * hadoop.security.token.service.use_ip=false
499 */
500 protected static class QualifiedHostResolver implements HostResolver {
501 @SuppressWarnings("unchecked")
502 private List<String> searchDomains =
503 ResolverConfiguration.open().searchlist();
504
505 /**
506 * Create an InetAddress with a fully qualified hostname of the given
507 * hostname. InetAddress does not qualify an incomplete hostname that
508 * is resolved via the domain search list.
509 * {@link InetAddress#getCanonicalHostName()} will fully qualify the
510 * hostname, but it always return the A record whereas the given hostname
511 * may be a CNAME.
512 *
513 * @param host a hostname or ip address
514 * @return InetAddress with the fully qualified hostname or ip
515 * @throws UnknownHostException if host does not exist
516 */
517 public InetAddress getByName(String host) throws UnknownHostException {
518 InetAddress addr = null;
519
520 if (IPAddressUtil.isIPv4LiteralAddress(host)) {
521 // use ipv4 address as-is
522 byte[] ip = IPAddressUtil.textToNumericFormatV4(host);
523 addr = InetAddress.getByAddress(host, ip);
524 } else if (IPAddressUtil.isIPv6LiteralAddress(host)) {
525 // use ipv6 address as-is
526 byte[] ip = IPAddressUtil.textToNumericFormatV6(host);
527 addr = InetAddress.getByAddress(host, ip);
528 } else if (host.endsWith(".")) {
529 // a rooted host ends with a dot, ex. "host."
530 // rooted hosts never use the search path, so only try an exact lookup
531 addr = getByExactName(host);
532 } else if (host.contains(".")) {
533 // the host contains a dot (domain), ex. "host.domain"
534 // try an exact host lookup, then fallback to search list
535 addr = getByExactName(host);
536 if (addr == null) {
537 addr = getByNameWithSearch(host);
538 }
539 } else {
540 // it's a simple host with no dots, ex. "host"
541 // try the search list, then fallback to exact host
542 InetAddress loopback = InetAddress.getByName(null);
543 if (host.equalsIgnoreCase(loopback.getHostName())) {
544 addr = InetAddress.getByAddress(host, loopback.getAddress());
545 } else {
546 addr = getByNameWithSearch(host);
547 if (addr == null) {
548 addr = getByExactName(host);
549 }
550 }
551 }
552 // unresolvable!
553 if (addr == null) {
554 throw new UnknownHostException(host);
555 }
556 return addr;
557 }
558
559 InetAddress getByExactName(String host) {
560 InetAddress addr = null;
561 // InetAddress will use the search list unless the host is rooted
562 // with a trailing dot. The trailing dot will disable any use of the
563 // search path in a lower level resolver. See RFC 1535.
564 String fqHost = host;
565 if (!fqHost.endsWith(".")) fqHost += ".";
566 try {
567 addr = getInetAddressByName(fqHost);
568 // can't leave the hostname as rooted or other parts of the system
569 // malfunction, ex. kerberos principals are lacking proper host
570 // equivalence for rooted/non-rooted hostnames
571 addr = InetAddress.getByAddress(host, addr.getAddress());
572 } catch (UnknownHostException e) {
573 // ignore, caller will throw if necessary
574 }
575 return addr;
576 }
577
578 InetAddress getByNameWithSearch(String host) {
579 InetAddress addr = null;
580 if (host.endsWith(".")) { // already qualified?
581 addr = getByExactName(host);
582 } else {
583 for (String domain : searchDomains) {
584 String dot = !domain.startsWith(".") ? "." : "";
585 addr = getByExactName(host + dot + domain);
586 if (addr != null) break;
587 }
588 }
589 return addr;
590 }
591
592 // implemented as a separate method to facilitate unit testing
593 InetAddress getInetAddressByName(String host) throws UnknownHostException {
594 return InetAddress.getByName(host);
595 }
596
597 void setSearchDomains(String ... domains) {
598 searchDomains = Arrays.asList(domains);
599 }
600 }
601 }