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