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