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