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 static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION; 020 021 import java.io.IOException; 022 import java.net.InetAddress; 023 import java.net.InetSocketAddress; 024 import java.net.URI; 025 import java.net.URL; 026 import java.net.URLConnection; 027 import java.net.UnknownHostException; 028 import java.security.AccessController; 029 import java.security.PrivilegedAction; 030 import java.security.PrivilegedExceptionAction; 031 import java.util.Arrays; 032 import java.util.List; 033 import java.util.ServiceLoader; 034 import java.util.Set; 035 036 import javax.security.auth.Subject; 037 import javax.security.auth.kerberos.KerberosPrincipal; 038 import javax.security.auth.kerberos.KerberosTicket; 039 040 import org.apache.commons.logging.Log; 041 import org.apache.commons.logging.LogFactory; 042 import org.apache.hadoop.classification.InterfaceAudience; 043 import org.apache.hadoop.classification.InterfaceStability; 044 import org.apache.hadoop.conf.Configuration; 045 import org.apache.hadoop.fs.CommonConfigurationKeys; 046 import org.apache.hadoop.http.HttpConfig; 047 import org.apache.hadoop.io.Text; 048 import org.apache.hadoop.net.NetUtils; 049 import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; 050 import org.apache.hadoop.security.authentication.client.AuthenticatedURL; 051 import org.apache.hadoop.security.authentication.client.AuthenticationException; 052 import org.apache.hadoop.security.ssl.SSLFactory; 053 import org.apache.hadoop.security.token.Token; 054 import org.apache.hadoop.security.token.TokenInfo; 055 056 import com.google.common.annotations.VisibleForTesting; 057 058 //this will need to be replaced someday when there is a suitable replacement 059 import sun.net.dns.ResolverConfiguration; 060 import sun.net.util.IPAddressUtil; 061 062 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) 063 @InterfaceStability.Evolving 064 public class SecurityUtil { 065 public static final Log LOG = LogFactory.getLog(SecurityUtil.class); 066 public static final String HOSTNAME_PATTERN = "_HOST"; 067 068 // controls whether buildTokenService will use an ip or host/ip as given 069 // by the user 070 @VisibleForTesting 071 static boolean useIpForTokenService; 072 @VisibleForTesting 073 static HostResolver hostResolver; 074 075 private static SSLFactory sslFactory; 076 077 static { 078 Configuration conf = new Configuration(); 079 boolean useIp = conf.getBoolean( 080 CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP, 081 CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT); 082 setTokenServiceUseIp(useIp); 083 if (HttpConfig.isSecure()) { 084 sslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, conf); 085 try { 086 sslFactory.init(); 087 } catch (Exception ex) { 088 throw new RuntimeException(ex); 089 } 090 } 091 } 092 093 /** 094 * For use only by tests and initialization 095 */ 096 @InterfaceAudience.Private 097 static void setTokenServiceUseIp(boolean flag) { 098 useIpForTokenService = flag; 099 hostResolver = !useIpForTokenService 100 ? new QualifiedHostResolver() 101 : new StandardHostResolver(); 102 } 103 104 /** 105 * Find the original TGT within the current subject's credentials. Cross-realm 106 * TGT's of the form "krbtgt/[email protected]" may be present. 107 * 108 * @return The TGT from the current subject 109 * @throws IOException 110 * if TGT can't be found 111 */ 112 private static KerberosTicket getTgtFromSubject() throws IOException { 113 Subject current = Subject.getSubject(AccessController.getContext()); 114 if (current == null) { 115 throw new IOException( 116 "Can't get TGT from current Subject, because it is null"); 117 } 118 Set<KerberosTicket> tickets = current 119 .getPrivateCredentials(KerberosTicket.class); 120 for (KerberosTicket t : tickets) { 121 if (isOriginalTGT(t)) 122 return t; 123 } 124 throw new IOException("Failed to find TGT from current Subject:"+current); 125 } 126 127 /** 128 * TGS must have the server principal of the form "krbtgt/FOO@FOO". 129 * @param principal 130 * @return true or false 131 */ 132 static boolean 133 isTGSPrincipal(KerberosPrincipal principal) { 134 if (principal == null) 135 return false; 136 if (principal.getName().equals("krbtgt/" + principal.getRealm() + 137 "@" + principal.getRealm())) { 138 return true; 139 } 140 return false; 141 } 142 143 /** 144 * Check whether the server principal is the TGS's principal 145 * @param ticket the original TGT (the ticket that is obtained when a 146 * kinit is done) 147 * @return true or false 148 */ 149 protected static boolean isOriginalTGT(KerberosTicket ticket) { 150 return isTGSPrincipal(ticket.getServer()); 151 } 152 153 /** 154 * Convert Kerberos principal name pattern to valid Kerberos principal 155 * names. It replaces hostname pattern with hostname, which should be 156 * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses 157 * dynamically looked-up fqdn of the current host instead. 158 * 159 * @param principalConfig 160 * the Kerberos principal name conf value to convert 161 * @param hostname 162 * the fully-qualified domain name used for substitution 163 * @return converted Kerberos principal name 164 * @throws IOException if the client address cannot be determined 165 */ 166 @InterfaceAudience.Public 167 @InterfaceStability.Evolving 168 public static String getServerPrincipal(String principalConfig, 169 String hostname) throws IOException { 170 String[] components = getComponents(principalConfig); 171 if (components == null || components.length != 3 172 || !components[1].equals(HOSTNAME_PATTERN)) { 173 return principalConfig; 174 } else { 175 return replacePattern(components, hostname); 176 } 177 } 178 179 /** 180 * Convert Kerberos principal name pattern to valid Kerberos principal names. 181 * This method is similar to {@link #getServerPrincipal(String, String)}, 182 * except 1) the reverse DNS lookup from addr to hostname is done only when 183 * necessary, 2) param addr can't be null (no default behavior of using local 184 * hostname when addr is null). 185 * 186 * @param principalConfig 187 * Kerberos principal name pattern to convert 188 * @param addr 189 * InetAddress of the host used for substitution 190 * @return converted Kerberos principal name 191 * @throws IOException if the client address cannot be determined 192 */ 193 @InterfaceAudience.Public 194 @InterfaceStability.Evolving 195 public static String getServerPrincipal(String principalConfig, 196 InetAddress addr) throws IOException { 197 String[] components = getComponents(principalConfig); 198 if (components == null || components.length != 3 199 || !components[1].equals(HOSTNAME_PATTERN)) { 200 return principalConfig; 201 } else { 202 if (addr == null) { 203 throw new IOException("Can't replace " + HOSTNAME_PATTERN 204 + " pattern since client address is null"); 205 } 206 return replacePattern(components, addr.getCanonicalHostName()); 207 } 208 } 209 210 private static String[] getComponents(String principalConfig) { 211 if (principalConfig == null) 212 return null; 213 return principalConfig.split("[/@]"); 214 } 215 216 private static String replacePattern(String[] components, String hostname) 217 throws IOException { 218 String fqdn = hostname; 219 if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) { 220 fqdn = getLocalHostName(); 221 } 222 return components[0] + "/" + fqdn.toLowerCase() + "@" + components[2]; 223 } 224 225 static String getLocalHostName() throws UnknownHostException { 226 return InetAddress.getLocalHost().getCanonicalHostName(); 227 } 228 229 /** 230 * Login as a principal specified in config. Substitute $host in 231 * user's Kerberos principal name with a dynamically looked-up fully-qualified 232 * domain name of the current host. 233 * 234 * @param conf 235 * conf to use 236 * @param keytabFileKey 237 * the key to look for keytab file in conf 238 * @param userNameKey 239 * the key to look for user's Kerberos principal name in conf 240 * @throws IOException if login fails 241 */ 242 @InterfaceAudience.Public 243 @InterfaceStability.Evolving 244 public static void login(final Configuration conf, 245 final String keytabFileKey, final String userNameKey) throws IOException { 246 login(conf, keytabFileKey, userNameKey, getLocalHostName()); 247 } 248 249 /** 250 * Login as a principal specified in config. Substitute $host in user's Kerberos principal 251 * name with hostname. If non-secure mode - return. If no keytab available - 252 * bail out with an exception 253 * 254 * @param conf 255 * conf to use 256 * @param keytabFileKey 257 * the key to look for keytab file in conf 258 * @param userNameKey 259 * the key to look for user's Kerberos principal name in conf 260 * @param hostname 261 * hostname to use for substitution 262 * @throws IOException if the config doesn't specify a keytab 263 */ 264 @InterfaceAudience.Public 265 @InterfaceStability.Evolving 266 public static void login(final Configuration conf, 267 final String keytabFileKey, final String userNameKey, String hostname) 268 throws IOException { 269 270 if(! UserGroupInformation.isSecurityEnabled()) 271 return; 272 273 String keytabFilename = conf.get(keytabFileKey); 274 if (keytabFilename == null || keytabFilename.length() == 0) { 275 throw new IOException("Running in secure mode, but config doesn't have a keytab"); 276 } 277 278 String principalConfig = conf.get(userNameKey, System 279 .getProperty("user.name")); 280 String principalName = SecurityUtil.getServerPrincipal(principalConfig, 281 hostname); 282 UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename); 283 } 284 285 /** 286 * create the service name for a Delegation token 287 * @param uri of the service 288 * @param defPort is used if the uri lacks a port 289 * @return the token service, or null if no authority 290 * @see #buildTokenService(InetSocketAddress) 291 */ 292 public static String buildDTServiceName(URI uri, int defPort) { 293 String authority = uri.getAuthority(); 294 if (authority == null) { 295 return null; 296 } 297 InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort); 298 return buildTokenService(addr).toString(); 299 } 300 301 /** 302 * Get the host name from the principal name of format <service>/host@realm. 303 * @param principalName principal name of format as described above 304 * @return host name if the the string conforms to the above format, else null 305 */ 306 public static String getHostFromPrincipal(String principalName) { 307 return new HadoopKerberosName(principalName).getHostName(); 308 } 309 310 private static ServiceLoader<SecurityInfo> securityInfoProviders = 311 ServiceLoader.load(SecurityInfo.class); 312 private static SecurityInfo[] testProviders = new SecurityInfo[0]; 313 314 /** 315 * Test setup method to register additional providers. 316 * @param providers a list of high priority providers to use 317 */ 318 @InterfaceAudience.Private 319 public static void setSecurityInfoProviders(SecurityInfo... providers) { 320 testProviders = providers; 321 } 322 323 /** 324 * Look up the KerberosInfo for a given protocol. It searches all known 325 * SecurityInfo providers. 326 * @param protocol the protocol class to get the information for 327 * @param conf configuration object 328 * @return the KerberosInfo or null if it has no KerberosInfo defined 329 */ 330 public static KerberosInfo 331 getKerberosInfo(Class<?> protocol, Configuration conf) { 332 synchronized (testProviders) { 333 for(SecurityInfo provider: testProviders) { 334 KerberosInfo result = provider.getKerberosInfo(protocol, conf); 335 if (result != null) { 336 return result; 337 } 338 } 339 } 340 341 synchronized (securityInfoProviders) { 342 for(SecurityInfo provider: securityInfoProviders) { 343 KerberosInfo result = provider.getKerberosInfo(protocol, conf); 344 if (result != null) { 345 return result; 346 } 347 } 348 } 349 return null; 350 } 351 352 /** 353 * Look up the TokenInfo for a given protocol. It searches all known 354 * SecurityInfo providers. 355 * @param protocol The protocol class to get the information for. 356 * @param conf Configuration object 357 * @return the TokenInfo or null if it has no KerberosInfo defined 358 */ 359 public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) { 360 synchronized (testProviders) { 361 for(SecurityInfo provider: testProviders) { 362 TokenInfo result = provider.getTokenInfo(protocol, conf); 363 if (result != null) { 364 return result; 365 } 366 } 367 } 368 369 synchronized (securityInfoProviders) { 370 for(SecurityInfo provider: securityInfoProviders) { 371 TokenInfo result = provider.getTokenInfo(protocol, conf); 372 if (result != null) { 373 return result; 374 } 375 } 376 } 377 378 return null; 379 } 380 381 /** 382 * Decode the given token's service field into an InetAddress 383 * @param token from which to obtain the service 384 * @return InetAddress for the service 385 */ 386 public static InetSocketAddress getTokenServiceAddr(Token<?> token) { 387 return NetUtils.createSocketAddr(token.getService().toString()); 388 } 389 390 /** 391 * Set the given token's service to the format expected by the RPC client 392 * @param token a delegation token 393 * @param addr the socket for the rpc connection 394 */ 395 public static void setTokenService(Token<?> token, InetSocketAddress addr) { 396 Text service = buildTokenService(addr); 397 if (token != null) { 398 token.setService(service); 399 if (LOG.isDebugEnabled()) { 400 LOG.debug("Acquired token "+token); // Token#toString() prints service 401 } 402 } else { 403 LOG.warn("Failed to get token for service "+service); 404 } 405 } 406 407 /** 408 * Construct the service key for a token 409 * @param addr InetSocketAddress of remote connection with a token 410 * @return "ip:port" or "host:port" depending on the value of 411 * hadoop.security.token.service.use_ip 412 */ 413 public static Text buildTokenService(InetSocketAddress addr) { 414 String host = null; 415 if (useIpForTokenService) { 416 if (addr.isUnresolved()) { // host has no ip address 417 throw new IllegalArgumentException( 418 new UnknownHostException(addr.getHostName()) 419 ); 420 } 421 host = addr.getAddress().getHostAddress(); 422 } else { 423 host = addr.getHostName().toLowerCase(); 424 } 425 return new Text(host + ":" + addr.getPort()); 426 } 427 428 /** 429 * Construct the service key for a token 430 * @param uri of remote connection with a token 431 * @return "ip:port" or "host:port" depending on the value of 432 * hadoop.security.token.service.use_ip 433 */ 434 public static Text buildTokenService(URI uri) { 435 return buildTokenService(NetUtils.createSocketAddr(uri.getAuthority())); 436 } 437 438 /** 439 * Perform the given action as the daemon's login user. If the login 440 * user cannot be determined, this will log a FATAL error and exit 441 * the whole JVM. 442 */ 443 public static <T> T doAsLoginUserOrFatal(PrivilegedAction<T> action) { 444 if (UserGroupInformation.isSecurityEnabled()) { 445 UserGroupInformation ugi = null; 446 try { 447 ugi = UserGroupInformation.getLoginUser(); 448 } catch (IOException e) { 449 LOG.fatal("Exception while getting login user", e); 450 e.printStackTrace(); 451 Runtime.getRuntime().exit(-1); 452 } 453 return ugi.doAs(action); 454 } else { 455 return action.run(); 456 } 457 } 458 459 /** 460 * Perform the given action as the daemon's login user. If an 461 * InterruptedException is thrown, it is converted to an IOException. 462 * 463 * @param action the action to perform 464 * @return the result of the action 465 * @throws IOException in the event of error 466 */ 467 public static <T> T doAsLoginUser(PrivilegedExceptionAction<T> action) 468 throws IOException { 469 return doAsUser(UserGroupInformation.getLoginUser(), action); 470 } 471 472 /** 473 * Perform the given action as the daemon's current user. If an 474 * InterruptedException is thrown, it is converted to an IOException. 475 * 476 * @param action the action to perform 477 * @return the result of the action 478 * @throws IOException in the event of error 479 */ 480 public static <T> T doAsCurrentUser(PrivilegedExceptionAction<T> action) 481 throws IOException { 482 return doAsUser(UserGroupInformation.getCurrentUser(), action); 483 } 484 485 private static <T> T doAsUser(UserGroupInformation ugi, 486 PrivilegedExceptionAction<T> action) throws IOException { 487 try { 488 return ugi.doAs(action); 489 } catch (InterruptedException ie) { 490 throw new IOException(ie); 491 } 492 } 493 494 /** 495 * Open a (if need be) secure connection to a URL in a secure environment 496 * that is using SPNEGO to authenticate its URLs. All Namenode and Secondary 497 * Namenode URLs that are protected via SPNEGO should be accessed via this 498 * method. 499 * 500 * @param url to authenticate via SPNEGO. 501 * @return A connection that has been authenticated via SPNEGO 502 * @throws IOException If unable to authenticate via SPNEGO 503 */ 504 public static URLConnection openSecureHttpConnection(URL url) throws IOException { 505 if (!HttpConfig.isSecure() && !UserGroupInformation.isSecurityEnabled()) { 506 return url.openConnection(); 507 } 508 509 AuthenticatedURL.Token token = new AuthenticatedURL.Token(); 510 try { 511 return new AuthenticatedURL(null, sslFactory).openConnection(url, token); 512 } catch (AuthenticationException e) { 513 throw new IOException("Exception trying to open authenticated connection to " 514 + url, e); 515 } 516 } 517 518 /** 519 * Resolves a host subject to the security requirements determined by 520 * hadoop.security.token.service.use_ip. 521 * 522 * @param hostname host or ip to resolve 523 * @return a resolved host 524 * @throws UnknownHostException if the host doesn't exist 525 */ 526 @InterfaceAudience.Private 527 public static 528 InetAddress getByName(String hostname) throws UnknownHostException { 529 return hostResolver.getByName(hostname); 530 } 531 532 interface HostResolver { 533 InetAddress getByName(String host) throws UnknownHostException; 534 } 535 536 /** 537 * Uses standard java host resolution 538 */ 539 static class StandardHostResolver implements HostResolver { 540 @Override 541 public InetAddress getByName(String host) throws UnknownHostException { 542 return InetAddress.getByName(host); 543 } 544 } 545 546 /** 547 * This an alternate resolver with important properties that the standard 548 * java resolver lacks: 549 * 1) The hostname is fully qualified. This avoids security issues if not 550 * all hosts in the cluster do not share the same search domains. It 551 * also prevents other hosts from performing unnecessary dns searches. 552 * In contrast, InetAddress simply returns the host as given. 553 * 2) The InetAddress is instantiated with an exact host and IP to prevent 554 * further unnecessary lookups. InetAddress may perform an unnecessary 555 * reverse lookup for an IP. 556 * 3) A call to getHostName() will always return the qualified hostname, or 557 * more importantly, the IP if instantiated with an IP. This avoids 558 * unnecessary dns timeouts if the host is not resolvable. 559 * 4) Point 3 also ensures that if the host is re-resolved, ex. during a 560 * connection re-attempt, that a reverse lookup to host and forward 561 * lookup to IP is not performed since the reverse/forward mappings may 562 * not always return the same IP. If the client initiated a connection 563 * with an IP, then that IP is all that should ever be contacted. 564 * 565 * NOTE: this resolver is only used if: 566 * hadoop.security.token.service.use_ip=false 567 */ 568 protected static class QualifiedHostResolver implements HostResolver { 569 @SuppressWarnings("unchecked") 570 private List<String> searchDomains = 571 ResolverConfiguration.open().searchlist(); 572 573 /** 574 * Create an InetAddress with a fully qualified hostname of the given 575 * hostname. InetAddress does not qualify an incomplete hostname that 576 * is resolved via the domain search list. 577 * {@link InetAddress#getCanonicalHostName()} will fully qualify the 578 * hostname, but it always return the A record whereas the given hostname 579 * may be a CNAME. 580 * 581 * @param host a hostname or ip address 582 * @return InetAddress with the fully qualified hostname or ip 583 * @throws UnknownHostException if host does not exist 584 */ 585 @Override 586 public InetAddress getByName(String host) throws UnknownHostException { 587 InetAddress addr = null; 588 589 if (IPAddressUtil.isIPv4LiteralAddress(host)) { 590 // use ipv4 address as-is 591 byte[] ip = IPAddressUtil.textToNumericFormatV4(host); 592 addr = InetAddress.getByAddress(host, ip); 593 } else if (IPAddressUtil.isIPv6LiteralAddress(host)) { 594 // use ipv6 address as-is 595 byte[] ip = IPAddressUtil.textToNumericFormatV6(host); 596 addr = InetAddress.getByAddress(host, ip); 597 } else if (host.endsWith(".")) { 598 // a rooted host ends with a dot, ex. "host." 599 // rooted hosts never use the search path, so only try an exact lookup 600 addr = getByExactName(host); 601 } else if (host.contains(".")) { 602 // the host contains a dot (domain), ex. "host.domain" 603 // try an exact host lookup, then fallback to search list 604 addr = getByExactName(host); 605 if (addr == null) { 606 addr = getByNameWithSearch(host); 607 } 608 } else { 609 // it's a simple host with no dots, ex. "host" 610 // try the search list, then fallback to exact host 611 InetAddress loopback = InetAddress.getByName(null); 612 if (host.equalsIgnoreCase(loopback.getHostName())) { 613 addr = InetAddress.getByAddress(host, loopback.getAddress()); 614 } else { 615 addr = getByNameWithSearch(host); 616 if (addr == null) { 617 addr = getByExactName(host); 618 } 619 } 620 } 621 // unresolvable! 622 if (addr == null) { 623 throw new UnknownHostException(host); 624 } 625 return addr; 626 } 627 628 InetAddress getByExactName(String host) { 629 InetAddress addr = null; 630 // InetAddress will use the search list unless the host is rooted 631 // with a trailing dot. The trailing dot will disable any use of the 632 // search path in a lower level resolver. See RFC 1535. 633 String fqHost = host; 634 if (!fqHost.endsWith(".")) fqHost += "."; 635 try { 636 addr = getInetAddressByName(fqHost); 637 // can't leave the hostname as rooted or other parts of the system 638 // malfunction, ex. kerberos principals are lacking proper host 639 // equivalence for rooted/non-rooted hostnames 640 addr = InetAddress.getByAddress(host, addr.getAddress()); 641 } catch (UnknownHostException e) { 642 // ignore, caller will throw if necessary 643 } 644 return addr; 645 } 646 647 InetAddress getByNameWithSearch(String host) { 648 InetAddress addr = null; 649 if (host.endsWith(".")) { // already qualified? 650 addr = getByExactName(host); 651 } else { 652 for (String domain : searchDomains) { 653 String dot = !domain.startsWith(".") ? "." : ""; 654 addr = getByExactName(host + dot + domain); 655 if (addr != null) break; 656 } 657 } 658 return addr; 659 } 660 661 // implemented as a separate method to facilitate unit testing 662 InetAddress getInetAddressByName(String host) throws UnknownHostException { 663 return InetAddress.getByName(host); 664 } 665 666 void setSearchDomains(String ... domains) { 667 searchDomains = Arrays.asList(domains); 668 } 669 } 670 671 public static AuthenticationMethod getAuthenticationMethod(Configuration conf) { 672 String value = conf.get(HADOOP_SECURITY_AUTHENTICATION, "simple"); 673 try { 674 return Enum.valueOf(AuthenticationMethod.class, value.toUpperCase()); 675 } catch (IllegalArgumentException iae) { 676 throw new IllegalArgumentException("Invalid attribute value for " + 677 HADOOP_SECURITY_AUTHENTICATION + " of " + value); 678 } 679 } 680 681 public static void setAuthenticationMethod( 682 AuthenticationMethod authenticationMethod, Configuration conf) { 683 if (authenticationMethod == null) { 684 authenticationMethod = AuthenticationMethod.SIMPLE; 685 } 686 conf.set(HADOOP_SECURITY_AUTHENTICATION, 687 authenticationMethod.toString().toLowerCase()); 688 } 689 }