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/TWO.COM@ONE.COM" 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 }