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