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