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