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