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.lang.reflect.Constructor; 021 import java.lang.reflect.Field; 022 import java.lang.reflect.InvocationTargetException; 023 import java.lang.reflect.Method; 024 import java.net.InetAddress; 025 import java.net.InetSocketAddress; 026 import java.net.URI; 027 import java.net.URL; 028 import java.net.UnknownHostException; 029 import java.security.AccessController; 030 import java.util.Arrays; 031 import java.util.List; 032 import java.util.ServiceLoader; 033 import java.util.Set; 034 035 import javax.security.auth.Subject; 036 import javax.security.auth.kerberos.KerberosPrincipal; 037 import javax.security.auth.kerberos.KerberosTicket; 038 039 import org.apache.commons.logging.Log; 040 import org.apache.commons.logging.LogFactory; 041 import org.apache.hadoop.classification.InterfaceAudience; 042 import org.apache.hadoop.classification.InterfaceStability; 043 import org.apache.hadoop.conf.Configuration; 044 import org.apache.hadoop.fs.CommonConfigurationKeys; 045 import org.apache.hadoop.io.Text; 046 import org.apache.hadoop.net.NetUtils; 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 * Explicitly pull the service ticket for the specified host. This solves a 138 * problem with Java's Kerberos SSL problem where the client cannot 139 * authenticate against a cross-realm service. It is necessary for clients 140 * making kerberized https requests to call this method on the target URL 141 * to ensure that in a cross-realm environment the remote host will be 142 * successfully authenticated. 143 * 144 * This method is internal to Hadoop and should not be used by other 145 * applications. This method should not be considered stable or open: 146 * it will be removed when the Java behavior is changed. 147 * 148 * @param remoteHost Target URL the krb-https client will access 149 * @throws IOException if the service ticket cannot be retrieved 150 */ 151 public static void fetchServiceTicket(URL remoteHost) throws IOException { 152 if(!UserGroupInformation.isSecurityEnabled()) 153 return; 154 155 String serviceName = "host/" + remoteHost.getHost(); 156 if (LOG.isDebugEnabled()) 157 LOG.debug("Fetching service ticket for host at: " + serviceName); 158 Object serviceCred = null; 159 Method credsToTicketMeth; 160 Class<?> krb5utilClass; 161 try { 162 Class<?> principalClass; 163 Class<?> credentialsClass; 164 165 if (System.getProperty("java.vendor").contains("IBM")) { 166 principalClass = Class.forName("com.ibm.security.krb5.PrincipalName"); 167 168 credentialsClass = Class.forName("com.ibm.security.krb5.Credentials"); 169 krb5utilClass = Class.forName("com.ibm.security.jgss.mech.krb5"); 170 } else { 171 principalClass = Class.forName("sun.security.krb5.PrincipalName"); 172 credentialsClass = Class.forName("sun.security.krb5.Credentials"); 173 krb5utilClass = Class.forName("sun.security.jgss.krb5.Krb5Util"); 174 } 175 @SuppressWarnings("rawtypes") 176 Constructor principalConstructor = principalClass.getConstructor(String.class, 177 int.class); 178 Field KRB_NT_SRV_HST = principalClass.getDeclaredField("KRB_NT_SRV_HST"); 179 Method acquireServiceCredsMeth = 180 credentialsClass.getDeclaredMethod("acquireServiceCreds", 181 String.class, credentialsClass); 182 Method ticketToCredsMeth = krb5utilClass.getDeclaredMethod("ticketToCreds", 183 KerberosTicket.class); 184 credsToTicketMeth = krb5utilClass.getDeclaredMethod("credsToTicket", 185 credentialsClass); 186 187 Object principal = principalConstructor.newInstance(serviceName, 188 KRB_NT_SRV_HST.get(principalClass)); 189 190 serviceCred = acquireServiceCredsMeth.invoke(credentialsClass, 191 principal.toString(), 192 ticketToCredsMeth.invoke(krb5utilClass, getTgtFromSubject())); 193 } catch (Exception e) { 194 throw new IOException("Can't get service ticket for: " 195 + serviceName, e); 196 } 197 if (serviceCred == null) { 198 throw new IOException("Can't get service ticket for " + serviceName); 199 } 200 try { 201 Subject.getSubject(AccessController.getContext()).getPrivateCredentials() 202 .add(credsToTicketMeth.invoke(krb5utilClass, serviceCred)); 203 } catch (Exception e) { 204 throw new IOException("Can't get service ticket for: " 205 + serviceName, e); 206 } 207 } 208 209 /** 210 * Convert Kerberos principal name pattern to valid Kerberos principal 211 * names. It replaces hostname pattern with hostname, which should be 212 * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses 213 * dynamically looked-up fqdn of the current host instead. 214 * 215 * @param principalConfig 216 * the Kerberos principal name conf value to convert 217 * @param hostname 218 * the fully-qualified domain name used for substitution 219 * @return converted Kerberos principal name 220 * @throws IOException if the client address cannot be determined 221 */ 222 public static String getServerPrincipal(String principalConfig, 223 String hostname) throws IOException { 224 String[] components = getComponents(principalConfig); 225 if (components == null || components.length != 3 226 || !components[1].equals(HOSTNAME_PATTERN)) { 227 return principalConfig; 228 } else { 229 return replacePattern(components, hostname); 230 } 231 } 232 233 /** 234 * Convert Kerberos principal name pattern to valid Kerberos principal names. 235 * This method is similar to {@link #getServerPrincipal(String, String)}, 236 * except 1) the reverse DNS lookup from addr to hostname is done only when 237 * necessary, 2) param addr can't be null (no default behavior of using local 238 * hostname when addr is null). 239 * 240 * @param principalConfig 241 * Kerberos principal name pattern to convert 242 * @param addr 243 * InetAddress of the host used for substitution 244 * @return converted Kerberos principal name 245 * @throws IOException if the client address cannot be determined 246 */ 247 public static String getServerPrincipal(String principalConfig, 248 InetAddress addr) throws IOException { 249 String[] components = getComponents(principalConfig); 250 if (components == null || components.length != 3 251 || !components[1].equals(HOSTNAME_PATTERN)) { 252 return principalConfig; 253 } else { 254 if (addr == null) { 255 throw new IOException("Can't replace " + HOSTNAME_PATTERN 256 + " pattern since client address is null"); 257 } 258 return replacePattern(components, addr.getCanonicalHostName()); 259 } 260 } 261 262 private static String[] getComponents(String principalConfig) { 263 if (principalConfig == null) 264 return null; 265 return principalConfig.split("[/@]"); 266 } 267 268 private static String replacePattern(String[] components, String hostname) 269 throws IOException { 270 String fqdn = hostname; 271 if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) { 272 fqdn = getLocalHostName(); 273 } 274 return components[0] + "/" + fqdn.toLowerCase() + "@" + components[2]; 275 } 276 277 static String getLocalHostName() throws UnknownHostException { 278 return InetAddress.getLocalHost().getCanonicalHostName(); 279 } 280 281 /** 282 * Login as a principal specified in config. Substitute $host in 283 * user's Kerberos principal name with a dynamically looked-up fully-qualified 284 * domain name of the current host. 285 * 286 * @param conf 287 * conf to use 288 * @param keytabFileKey 289 * the key to look for keytab file in conf 290 * @param userNameKey 291 * the key to look for user's Kerberos principal name in conf 292 * @throws IOException if login fails 293 */ 294 public static void login(final Configuration conf, 295 final String keytabFileKey, final String userNameKey) throws IOException { 296 login(conf, keytabFileKey, userNameKey, getLocalHostName()); 297 } 298 299 /** 300 * Login as a principal specified in config. Substitute $host in user's Kerberos principal 301 * name with hostname. If non-secure mode - return. If no keytab available - 302 * bail out with an exception 303 * 304 * @param conf 305 * conf to use 306 * @param keytabFileKey 307 * the key to look for keytab file in conf 308 * @param userNameKey 309 * the key to look for user's Kerberos principal name in conf 310 * @param hostname 311 * hostname to use for substitution 312 * @throws IOException if the config doesn't specify a keytab 313 */ 314 public static void login(final Configuration conf, 315 final String keytabFileKey, final String userNameKey, String hostname) 316 throws IOException { 317 318 if(! UserGroupInformation.isSecurityEnabled()) 319 return; 320 321 String keytabFilename = conf.get(keytabFileKey); 322 if (keytabFilename == null || keytabFilename.length() == 0) { 323 throw new IOException("Running in secure mode, but config doesn't have a keytab"); 324 } 325 326 String principalConfig = conf.get(userNameKey, System 327 .getProperty("user.name")); 328 String principalName = SecurityUtil.getServerPrincipal(principalConfig, 329 hostname); 330 UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename); 331 } 332 333 /** 334 * create the service name for a Delegation token 335 * @param uri of the service 336 * @param defPort is used if the uri lacks a port 337 * @return the token service, or null if no authority 338 * @see #buildTokenService(InetSocketAddress) 339 */ 340 public static String buildDTServiceName(URI uri, int defPort) { 341 String authority = uri.getAuthority(); 342 if (authority == null) { 343 return null; 344 } 345 InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort); 346 return buildTokenService(addr).toString(); 347 } 348 349 /** 350 * Get the host name from the principal name of format <service>/host@realm. 351 * @param principalName principal name of format as described above 352 * @return host name if the the string conforms to the above format, else null 353 */ 354 public static String getHostFromPrincipal(String principalName) { 355 return new HadoopKerberosName(principalName).getHostName(); 356 } 357 358 private static ServiceLoader<SecurityInfo> securityInfoProviders = 359 ServiceLoader.load(SecurityInfo.class); 360 private static SecurityInfo[] testProviders = new SecurityInfo[0]; 361 362 /** 363 * Test setup method to register additional providers. 364 * @param providers a list of high priority providers to use 365 */ 366 @InterfaceAudience.Private 367 public static void setSecurityInfoProviders(SecurityInfo... providers) { 368 testProviders = providers; 369 } 370 371 /** 372 * Look up the KerberosInfo for a given protocol. It searches all known 373 * SecurityInfo providers. 374 * @param protocol the protocol class to get the information for 375 * @param conf configuration object 376 * @return the KerberosInfo or null if it has no KerberosInfo defined 377 */ 378 public static KerberosInfo 379 getKerberosInfo(Class<?> protocol, Configuration conf) { 380 synchronized (testProviders) { 381 for(SecurityInfo provider: testProviders) { 382 KerberosInfo result = provider.getKerberosInfo(protocol, conf); 383 if (result != null) { 384 return result; 385 } 386 } 387 } 388 389 synchronized (securityInfoProviders) { 390 for(SecurityInfo provider: securityInfoProviders) { 391 KerberosInfo result = provider.getKerberosInfo(protocol, conf); 392 if (result != null) { 393 return result; 394 } 395 } 396 } 397 return null; 398 } 399 400 /** 401 * Look up the TokenInfo for a given protocol. It searches all known 402 * SecurityInfo providers. 403 * @param protocol The protocol class to get the information for. 404 * @param conf Configuration object 405 * @return the TokenInfo or null if it has no KerberosInfo defined 406 */ 407 public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) { 408 synchronized (testProviders) { 409 for(SecurityInfo provider: testProviders) { 410 TokenInfo result = provider.getTokenInfo(protocol, conf); 411 if (result != null) { 412 return result; 413 } 414 } 415 } 416 417 synchronized (securityInfoProviders) { 418 for(SecurityInfo provider: securityInfoProviders) { 419 TokenInfo result = provider.getTokenInfo(protocol, conf); 420 if (result != null) { 421 return result; 422 } 423 } 424 } 425 426 return null; 427 } 428 429 /** 430 * Decode the given token's service field into an InetAddress 431 * @param token from which to obtain the service 432 * @return InetAddress for the service 433 */ 434 public static InetSocketAddress getTokenServiceAddr(Token<?> token) { 435 return NetUtils.createSocketAddr(token.getService().toString()); 436 } 437 438 /** 439 * Set the given token's service to the format expected by the RPC client 440 * @param token a delegation token 441 * @param addr the socket for the rpc connection 442 */ 443 public static void setTokenService(Token<?> token, InetSocketAddress addr) { 444 Text service = buildTokenService(addr); 445 if (token != null) { 446 token.setService(service); 447 if (LOG.isDebugEnabled()) { 448 LOG.debug("Acquired token "+token); // Token#toString() prints service 449 } 450 } else { 451 LOG.warn("Failed to get token for service "+service); 452 } 453 } 454 455 /** 456 * Construct the service key for a token 457 * @param addr InetSocketAddress of remote connection with a token 458 * @return "ip:port" or "host:port" depending on the value of 459 * hadoop.security.token.service.use_ip 460 */ 461 public static Text buildTokenService(InetSocketAddress addr) { 462 String host = null; 463 if (useIpForTokenService) { 464 if (addr.isUnresolved()) { // host has no ip address 465 throw new IllegalArgumentException( 466 new UnknownHostException(addr.getHostName()) 467 ); 468 } 469 host = addr.getAddress().getHostAddress(); 470 } else { 471 host = addr.getHostName().toLowerCase(); 472 } 473 return new Text(host + ":" + addr.getPort()); 474 } 475 476 /** 477 * Construct the service key for a token 478 * @param uri of remote connection with a token 479 * @return "ip:port" or "host:port" depending on the value of 480 * hadoop.security.token.service.use_ip 481 */ 482 public static Text buildTokenService(URI uri) { 483 return buildTokenService(NetUtils.createSocketAddr(uri.getAuthority())); 484 } 485 486 /** 487 * Resolves a host subject to the security requirements determined by 488 * hadoop.security.token.service.use_ip. 489 * 490 * @param hostname host or ip to resolve 491 * @return a resolved host 492 * @throws UnknownHostException if the host doesn't exist 493 */ 494 @InterfaceAudience.Private 495 public static 496 InetAddress getByName(String hostname) throws UnknownHostException { 497 return hostResolver.getByName(hostname); 498 } 499 500 interface HostResolver { 501 InetAddress getByName(String host) throws UnknownHostException; 502 } 503 504 /** 505 * Uses standard java host resolution 506 */ 507 static class StandardHostResolver implements HostResolver { 508 public InetAddress getByName(String host) throws UnknownHostException { 509 return InetAddress.getByName(host); 510 } 511 } 512 513 /** 514 * This an alternate resolver with important properties that the standard 515 * java resolver lacks: 516 * 1) The hostname is fully qualified. This avoids security issues if not 517 * all hosts in the cluster do not share the same search domains. It 518 * also prevents other hosts from performing unnecessary dns searches. 519 * In contrast, InetAddress simply returns the host as given. 520 * 2) The InetAddress is instantiated with an exact host and IP to prevent 521 * further unnecessary lookups. InetAddress may perform an unnecessary 522 * reverse lookup for an IP. 523 * 3) A call to getHostName() will always return the qualified hostname, or 524 * more importantly, the IP if instantiated with an IP. This avoids 525 * unnecessary dns timeouts if the host is not resolvable. 526 * 4) Point 3 also ensures that if the host is re-resolved, ex. during a 527 * connection re-attempt, that a reverse lookup to host and forward 528 * lookup to IP is not performed since the reverse/forward mappings may 529 * not always return the same IP. If the client initiated a connection 530 * with an IP, then that IP is all that should ever be contacted. 531 * 532 * NOTE: this resolver is only used if: 533 * hadoop.security.token.service.use_ip=false 534 */ 535 protected static class QualifiedHostResolver implements HostResolver { 536 @SuppressWarnings("unchecked") 537 private List<String> searchDomains = 538 ResolverConfiguration.open().searchlist(); 539 540 /** 541 * Create an InetAddress with a fully qualified hostname of the given 542 * hostname. InetAddress does not qualify an incomplete hostname that 543 * is resolved via the domain search list. 544 * {@link InetAddress#getCanonicalHostName()} will fully qualify the 545 * hostname, but it always return the A record whereas the given hostname 546 * may be a CNAME. 547 * 548 * @param host a hostname or ip address 549 * @return InetAddress with the fully qualified hostname or ip 550 * @throws UnknownHostException if host does not exist 551 */ 552 public InetAddress getByName(String host) throws UnknownHostException { 553 InetAddress addr = null; 554 555 if (IPAddressUtil.isIPv4LiteralAddress(host)) { 556 // use ipv4 address as-is 557 byte[] ip = IPAddressUtil.textToNumericFormatV4(host); 558 addr = InetAddress.getByAddress(host, ip); 559 } else if (IPAddressUtil.isIPv6LiteralAddress(host)) { 560 // use ipv6 address as-is 561 byte[] ip = IPAddressUtil.textToNumericFormatV6(host); 562 addr = InetAddress.getByAddress(host, ip); 563 } else if (host.endsWith(".")) { 564 // a rooted host ends with a dot, ex. "host." 565 // rooted hosts never use the search path, so only try an exact lookup 566 addr = getByExactName(host); 567 } else if (host.contains(".")) { 568 // the host contains a dot (domain), ex. "host.domain" 569 // try an exact host lookup, then fallback to search list 570 addr = getByExactName(host); 571 if (addr == null) { 572 addr = getByNameWithSearch(host); 573 } 574 } else { 575 // it's a simple host with no dots, ex. "host" 576 // try the search list, then fallback to exact host 577 InetAddress loopback = InetAddress.getByName(null); 578 if (host.equalsIgnoreCase(loopback.getHostName())) { 579 addr = InetAddress.getByAddress(host, loopback.getAddress()); 580 } else { 581 addr = getByNameWithSearch(host); 582 if (addr == null) { 583 addr = getByExactName(host); 584 } 585 } 586 } 587 // unresolvable! 588 if (addr == null) { 589 throw new UnknownHostException(host); 590 } 591 return addr; 592 } 593 594 InetAddress getByExactName(String host) { 595 InetAddress addr = null; 596 // InetAddress will use the search list unless the host is rooted 597 // with a trailing dot. The trailing dot will disable any use of the 598 // search path in a lower level resolver. See RFC 1535. 599 String fqHost = host; 600 if (!fqHost.endsWith(".")) fqHost += "."; 601 try { 602 addr = getInetAddressByName(fqHost); 603 // can't leave the hostname as rooted or other parts of the system 604 // malfunction, ex. kerberos principals are lacking proper host 605 // equivalence for rooted/non-rooted hostnames 606 addr = InetAddress.getByAddress(host, addr.getAddress()); 607 } catch (UnknownHostException e) { 608 // ignore, caller will throw if necessary 609 } 610 return addr; 611 } 612 613 InetAddress getByNameWithSearch(String host) { 614 InetAddress addr = null; 615 if (host.endsWith(".")) { // already qualified? 616 addr = getByExactName(host); 617 } else { 618 for (String domain : searchDomains) { 619 String dot = !domain.startsWith(".") ? "." : ""; 620 addr = getByExactName(host + dot + domain); 621 if (addr != null) break; 622 } 623 } 624 return addr; 625 } 626 627 // implemented as a separate method to facilitate unit testing 628 InetAddress getInetAddressByName(String host) throws UnknownHostException { 629 return InetAddress.getByName(host); 630 } 631 632 void setSearchDomains(String ... domains) { 633 searchDomains = Arrays.asList(domains); 634 } 635 } 636 }