1 /** 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with this 4 * work for additional information regarding copyright ownership. The ASF 5 * licenses this file to you under the Apache License, Version 2.0 (the 6 * "License"); you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 * License for the specific language governing permissions and limitations under 15 * the License. 16 */ 17 package org.apache.hadoop.security; 18 19 import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION; 20 21 import java.io.IOException; 22 import java.net.InetAddress; 23 import java.net.InetSocketAddress; 24 import java.net.URI; 25 import java.net.UnknownHostException; 26 import java.security.PrivilegedAction; 27 import java.security.PrivilegedExceptionAction; 28 import java.util.Arrays; 29 import java.util.List; 30 import java.util.ServiceLoader; 31 32 import javax.security.auth.kerberos.KerberosPrincipal; 33 import javax.security.auth.kerberos.KerberosTicket; 34 35 import org.apache.commons.logging.Log; 36 import org.apache.commons.logging.LogFactory; 37 import org.apache.hadoop.classification.InterfaceAudience; 38 import org.apache.hadoop.classification.InterfaceStability; 39 import org.apache.hadoop.conf.Configuration; 40 import org.apache.hadoop.fs.CommonConfigurationKeys; 41 import org.apache.hadoop.io.Text; 42 import org.apache.hadoop.net.NetUtils; 43 import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; 44 import org.apache.hadoop.security.token.Token; 45 import org.apache.hadoop.security.token.TokenInfo; 46 import org.apache.hadoop.util.StringUtils; 47 48 49 //this will need to be replaced someday when there is a suitable replacement 50 import sun.net.dns.ResolverConfiguration; 51 import sun.net.util.IPAddressUtil; 52 53 import com.google.common.annotations.VisibleForTesting; 54 55 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) 56 @InterfaceStability.Evolving 57 public class SecurityUtil { 58 public static final Log LOG = LogFactory.getLog(SecurityUtil.class); 59 public static final String HOSTNAME_PATTERN = "_HOST"; 60 public static final String FAILED_TO_GET_UGI_MSG_HEADER = 61 "Failed to obtain user group information:"; 62 63 // controls whether buildTokenService will use an ip or host/ip as given 64 // by the user 65 @VisibleForTesting 66 static boolean useIpForTokenService; 67 @VisibleForTesting 68 static HostResolver hostResolver; 69 70 static { 71 Configuration conf = new Configuration(); 72 boolean useIp = conf.getBoolean( 73 CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP, 74 CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT); 75 setTokenServiceUseIp(useIp); 76 } 77 78 /** 79 * For use only by tests and initialization 80 */ 81 @InterfaceAudience.Private 82 @VisibleForTesting setTokenServiceUseIp(boolean flag)83 public static void setTokenServiceUseIp(boolean flag) { 84 useIpForTokenService = flag; 85 hostResolver = !useIpForTokenService 86 ? new QualifiedHostResolver() 87 : new StandardHostResolver(); 88 } 89 90 /** 91 * TGS must have the server principal of the form "krbtgt/FOO@FOO". 92 * @param principal 93 * @return true or false 94 */ 95 static boolean isTGSPrincipal(KerberosPrincipal principal)96 isTGSPrincipal(KerberosPrincipal principal) { 97 if (principal == null) 98 return false; 99 if (principal.getName().equals("krbtgt/" + principal.getRealm() + 100 "@" + principal.getRealm())) { 101 return true; 102 } 103 return false; 104 } 105 106 /** 107 * Check whether the server principal is the TGS's principal 108 * @param ticket the original TGT (the ticket that is obtained when a 109 * kinit is done) 110 * @return true or false 111 */ isOriginalTGT(KerberosTicket ticket)112 protected static boolean isOriginalTGT(KerberosTicket ticket) { 113 return isTGSPrincipal(ticket.getServer()); 114 } 115 116 /** 117 * Convert Kerberos principal name pattern to valid Kerberos principal 118 * names. It replaces hostname pattern with hostname, which should be 119 * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses 120 * dynamically looked-up fqdn of the current host instead. 121 * 122 * @param principalConfig 123 * the Kerberos principal name conf value to convert 124 * @param hostname 125 * the fully-qualified domain name used for substitution 126 * @return converted Kerberos principal name 127 * @throws IOException if the client address cannot be determined 128 */ 129 @InterfaceAudience.Public 130 @InterfaceStability.Evolving getServerPrincipal(String principalConfig, String hostname)131 public static String getServerPrincipal(String principalConfig, 132 String hostname) throws IOException { 133 String[] components = getComponents(principalConfig); 134 if (components == null || components.length != 3 135 || !components[1].equals(HOSTNAME_PATTERN)) { 136 return principalConfig; 137 } else { 138 return replacePattern(components, hostname); 139 } 140 } 141 142 /** 143 * Convert Kerberos principal name pattern to valid Kerberos principal names. 144 * This method is similar to {@link #getServerPrincipal(String, String)}, 145 * except 1) the reverse DNS lookup from addr to hostname is done only when 146 * necessary, 2) param addr can't be null (no default behavior of using local 147 * hostname when addr is null). 148 * 149 * @param principalConfig 150 * Kerberos principal name pattern to convert 151 * @param addr 152 * InetAddress of the host used for substitution 153 * @return converted Kerberos principal name 154 * @throws IOException if the client address cannot be determined 155 */ 156 @InterfaceAudience.Public 157 @InterfaceStability.Evolving getServerPrincipal(String principalConfig, InetAddress addr)158 public static String getServerPrincipal(String principalConfig, 159 InetAddress addr) throws IOException { 160 String[] components = getComponents(principalConfig); 161 if (components == null || components.length != 3 162 || !components[1].equals(HOSTNAME_PATTERN)) { 163 return principalConfig; 164 } else { 165 if (addr == null) { 166 throw new IOException("Can't replace " + HOSTNAME_PATTERN 167 + " pattern since client address is null"); 168 } 169 return replacePattern(components, addr.getCanonicalHostName()); 170 } 171 } 172 getComponents(String principalConfig)173 private static String[] getComponents(String principalConfig) { 174 if (principalConfig == null) 175 return null; 176 return principalConfig.split("[/@]"); 177 } 178 replacePattern(String[] components, String hostname)179 private static String replacePattern(String[] components, String hostname) 180 throws IOException { 181 String fqdn = hostname; 182 if (fqdn == null || fqdn.isEmpty() || fqdn.equals("0.0.0.0")) { 183 fqdn = getLocalHostName(); 184 } 185 return components[0] + "/" + 186 StringUtils.toLowerCase(fqdn) + "@" + components[2]; 187 } 188 getLocalHostName()189 static String getLocalHostName() throws UnknownHostException { 190 return InetAddress.getLocalHost().getCanonicalHostName(); 191 } 192 193 /** 194 * Login as a principal specified in config. Substitute $host in 195 * user's Kerberos principal name with a dynamically looked-up fully-qualified 196 * domain name of the current host. 197 * 198 * @param conf 199 * conf to use 200 * @param keytabFileKey 201 * the key to look for keytab file in conf 202 * @param userNameKey 203 * the key to look for user's Kerberos principal name in conf 204 * @throws IOException if login fails 205 */ 206 @InterfaceAudience.Public 207 @InterfaceStability.Evolving login(final Configuration conf, final String keytabFileKey, final String userNameKey)208 public static void login(final Configuration conf, 209 final String keytabFileKey, final String userNameKey) throws IOException { 210 login(conf, keytabFileKey, userNameKey, getLocalHostName()); 211 } 212 213 /** 214 * Login as a principal specified in config. Substitute $host in user's Kerberos principal 215 * name with hostname. If non-secure mode - return. If no keytab available - 216 * bail out with an exception 217 * 218 * @param conf 219 * conf to use 220 * @param keytabFileKey 221 * the key to look for keytab file in conf 222 * @param userNameKey 223 * the key to look for user's Kerberos principal name in conf 224 * @param hostname 225 * hostname to use for substitution 226 * @throws IOException if the config doesn't specify a keytab 227 */ 228 @InterfaceAudience.Public 229 @InterfaceStability.Evolving login(final Configuration conf, final String keytabFileKey, final String userNameKey, String hostname)230 public static void login(final Configuration conf, 231 final String keytabFileKey, final String userNameKey, String hostname) 232 throws IOException { 233 234 if(! UserGroupInformation.isSecurityEnabled()) 235 return; 236 237 String keytabFilename = conf.get(keytabFileKey); 238 if (keytabFilename == null || keytabFilename.length() == 0) { 239 throw new IOException("Running in secure mode, but config doesn't have a keytab"); 240 } 241 242 String principalConfig = conf.get(userNameKey, System 243 .getProperty("user.name")); 244 String principalName = SecurityUtil.getServerPrincipal(principalConfig, 245 hostname); 246 UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename); 247 } 248 249 /** 250 * create the service name for a Delegation token 251 * @param uri of the service 252 * @param defPort is used if the uri lacks a port 253 * @return the token service, or null if no authority 254 * @see #buildTokenService(InetSocketAddress) 255 */ buildDTServiceName(URI uri, int defPort)256 public static String buildDTServiceName(URI uri, int defPort) { 257 String authority = uri.getAuthority(); 258 if (authority == null) { 259 return null; 260 } 261 InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort); 262 return buildTokenService(addr).toString(); 263 } 264 265 /** 266 * Get the host name from the principal name of format <service>/host@realm. 267 * @param principalName principal name of format as described above 268 * @return host name if the the string conforms to the above format, else null 269 */ getHostFromPrincipal(String principalName)270 public static String getHostFromPrincipal(String principalName) { 271 return new HadoopKerberosName(principalName).getHostName(); 272 } 273 274 private static ServiceLoader<SecurityInfo> securityInfoProviders = 275 ServiceLoader.load(SecurityInfo.class); 276 private static SecurityInfo[] testProviders = new SecurityInfo[0]; 277 278 /** 279 * Test setup method to register additional providers. 280 * @param providers a list of high priority providers to use 281 */ 282 @InterfaceAudience.Private setSecurityInfoProviders(SecurityInfo... providers)283 public static void setSecurityInfoProviders(SecurityInfo... providers) { 284 testProviders = providers; 285 } 286 287 /** 288 * Look up the KerberosInfo for a given protocol. It searches all known 289 * SecurityInfo providers. 290 * @param protocol the protocol class to get the information for 291 * @param conf configuration object 292 * @return the KerberosInfo or null if it has no KerberosInfo defined 293 */ 294 public static KerberosInfo getKerberosInfo(Class<?> protocol, Configuration conf)295 getKerberosInfo(Class<?> protocol, Configuration conf) { 296 for(SecurityInfo provider: testProviders) { 297 KerberosInfo result = provider.getKerberosInfo(protocol, conf); 298 if (result != null) { 299 return result; 300 } 301 } 302 303 synchronized (securityInfoProviders) { 304 for(SecurityInfo provider: securityInfoProviders) { 305 KerberosInfo result = provider.getKerberosInfo(protocol, conf); 306 if (result != null) { 307 return result; 308 } 309 } 310 } 311 return null; 312 } 313 314 /** 315 * Look up the TokenInfo for a given protocol. It searches all known 316 * SecurityInfo providers. 317 * @param protocol The protocol class to get the information for. 318 * @param conf Configuration object 319 * @return the TokenInfo or null if it has no KerberosInfo defined 320 */ getTokenInfo(Class<?> protocol, Configuration conf)321 public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) { 322 for(SecurityInfo provider: testProviders) { 323 TokenInfo result = provider.getTokenInfo(protocol, conf); 324 if (result != null) { 325 return result; 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 */ getTokenServiceAddr(Token<?> token)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 */ setTokenService(Token<?> token, InetSocketAddress addr)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 */ buildTokenService(InetSocketAddress addr)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 = StringUtils.toLowerCase(addr.getHostName()); 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 */ buildTokenService(URI uri)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 */ doAsLoginUserOrFatal(PrivilegedAction<T> action)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 */ doAsLoginUser(PrivilegedExceptionAction<T> action)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 */ doAsCurrentUser(PrivilegedExceptionAction<T> action)440 public static <T> T doAsCurrentUser(PrivilegedExceptionAction<T> action) 441 throws IOException { 442 return doAsUser(UserGroupInformation.getCurrentUser(), action); 443 } 444 doAsUser(UserGroupInformation ugi, PrivilegedExceptionAction<T> action)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 getByName(String hostname)464 InetAddress getByName(String hostname) throws UnknownHostException { 465 return hostResolver.getByName(hostname); 466 } 467 468 interface HostResolver { getByName(String host)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 getByName(String host)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 getByName(String host)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 getByExactName(String host)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 getByNameWithSearch(String host)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 getInetAddressByName(String host)598 InetAddress getInetAddressByName(String host) throws UnknownHostException { 599 return InetAddress.getByName(host); 600 } 601 setSearchDomains(String .... domains)602 void setSearchDomains(String ... domains) { 603 searchDomains = Arrays.asList(domains); 604 } 605 } 606 getAuthenticationMethod(Configuration conf)607 public static AuthenticationMethod getAuthenticationMethod(Configuration conf) { 608 String value = conf.get(HADOOP_SECURITY_AUTHENTICATION, "simple"); 609 try { 610 return Enum.valueOf(AuthenticationMethod.class, 611 StringUtils.toUpperCase(value)); 612 } catch (IllegalArgumentException iae) { 613 throw new IllegalArgumentException("Invalid attribute value for " + 614 HADOOP_SECURITY_AUTHENTICATION + " of " + value); 615 } 616 } 617 setAuthenticationMethod( AuthenticationMethod authenticationMethod, Configuration conf)618 public static void setAuthenticationMethod( 619 AuthenticationMethod authenticationMethod, Configuration conf) { 620 if (authenticationMethod == null) { 621 authenticationMethod = AuthenticationMethod.SIMPLE; 622 } 623 conf.set(HADOOP_SECURITY_AUTHENTICATION, 624 StringUtils.toLowerCase(authenticationMethod.toString())); 625 } 626 627 /* 628 * Check if a given port is privileged. 629 * The ports with number smaller than 1024 are treated as privileged ports in 630 * unix/linux system. For other operating systems, use this method with care. 631 * For example, Windows doesn't have the concept of privileged ports. 632 * However, it may be used at Windows client to check port of linux server. 633 * 634 * @param port the port number 635 * @return true for privileged ports, false otherwise 636 * 637 */ isPrivilegedPort(final int port)638 public static boolean isPrivilegedPort(final int port) { 639 return port < 1024; 640 } 641 } 642