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