1 /**
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements. See the NOTICE file distributed with this
4  * work for additional information regarding copyright ownership. The ASF
5  * licenses this file to you under the Apache License, Version 2.0 (the
6  * "License"); you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14  * License for the specific language governing permissions and limitations under
15  * the License.
16  */
17 package org.apache.hadoop.security;
18 
19 import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION;
20 
21 import java.io.IOException;
22 import java.net.InetAddress;
23 import java.net.InetSocketAddress;
24 import java.net.URI;
25 import java.net.UnknownHostException;
26 import java.security.PrivilegedAction;
27 import java.security.PrivilegedExceptionAction;
28 import java.util.Arrays;
29 import java.util.List;
30 import java.util.ServiceLoader;
31 
32 import javax.security.auth.kerberos.KerberosPrincipal;
33 import javax.security.auth.kerberos.KerberosTicket;
34 
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37 import org.apache.hadoop.classification.InterfaceAudience;
38 import org.apache.hadoop.classification.InterfaceStability;
39 import org.apache.hadoop.conf.Configuration;
40 import org.apache.hadoop.fs.CommonConfigurationKeys;
41 import org.apache.hadoop.io.Text;
42 import org.apache.hadoop.net.NetUtils;
43 import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
44 import org.apache.hadoop.security.token.Token;
45 import org.apache.hadoop.security.token.TokenInfo;
46 import org.apache.hadoop.util.StringUtils;
47 
48 
49 //this will need to be replaced someday when there is a suitable replacement
50 import sun.net.dns.ResolverConfiguration;
51 import sun.net.util.IPAddressUtil;
52 
53 import com.google.common.annotations.VisibleForTesting;
54 
55 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
56 @InterfaceStability.Evolving
57 public class SecurityUtil {
58   public static final Log LOG = LogFactory.getLog(SecurityUtil.class);
59   public static final String HOSTNAME_PATTERN = "_HOST";
60   public static final String FAILED_TO_GET_UGI_MSG_HEADER =
61       "Failed to obtain user group information:";
62 
63   // controls whether buildTokenService will use an ip or host/ip as given
64   // by the user
65   @VisibleForTesting
66   static boolean useIpForTokenService;
67   @VisibleForTesting
68   static HostResolver hostResolver;
69 
70   static {
71     Configuration conf = new Configuration();
72     boolean useIp = conf.getBoolean(
73         CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP,
74         CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT);
75     setTokenServiceUseIp(useIp);
76   }
77 
78   /**
79    * For use only by tests and initialization
80    */
81   @InterfaceAudience.Private
82   @VisibleForTesting
setTokenServiceUseIp(boolean flag)83   public static void setTokenServiceUseIp(boolean flag) {
84     useIpForTokenService = flag;
85     hostResolver = !useIpForTokenService
86         ? new QualifiedHostResolver()
87         : new StandardHostResolver();
88   }
89 
90   /**
91    * TGS must have the server principal of the form "krbtgt/FOO@FOO".
92    * @param principal
93    * @return true or false
94    */
95   static boolean
isTGSPrincipal(KerberosPrincipal principal)96   isTGSPrincipal(KerberosPrincipal principal) {
97     if (principal == null)
98       return false;
99     if (principal.getName().equals("krbtgt/" + principal.getRealm() +
100         "@" + principal.getRealm())) {
101       return true;
102     }
103     return false;
104   }
105 
106   /**
107    * Check whether the server principal is the TGS's principal
108    * @param ticket the original TGT (the ticket that is obtained when a
109    * kinit is done)
110    * @return true or false
111    */
isOriginalTGT(KerberosTicket ticket)112   protected static boolean isOriginalTGT(KerberosTicket ticket) {
113     return isTGSPrincipal(ticket.getServer());
114   }
115 
116   /**
117    * Convert Kerberos principal name pattern to valid Kerberos principal
118    * names. It replaces hostname pattern with hostname, which should be
119    * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses
120    * dynamically looked-up fqdn of the current host instead.
121    *
122    * @param principalConfig
123    *          the Kerberos principal name conf value to convert
124    * @param hostname
125    *          the fully-qualified domain name used for substitution
126    * @return converted Kerberos principal name
127    * @throws IOException if the client address cannot be determined
128    */
129   @InterfaceAudience.Public
130   @InterfaceStability.Evolving
getServerPrincipal(String principalConfig, String hostname)131   public static String getServerPrincipal(String principalConfig,
132       String hostname) throws IOException {
133     String[] components = getComponents(principalConfig);
134     if (components == null || components.length != 3
135         || !components[1].equals(HOSTNAME_PATTERN)) {
136       return principalConfig;
137     } else {
138       return replacePattern(components, hostname);
139     }
140   }
141 
142   /**
143    * Convert Kerberos principal name pattern to valid Kerberos principal names.
144    * This method is similar to {@link #getServerPrincipal(String, String)},
145    * except 1) the reverse DNS lookup from addr to hostname is done only when
146    * necessary, 2) param addr can't be null (no default behavior of using local
147    * hostname when addr is null).
148    *
149    * @param principalConfig
150    *          Kerberos principal name pattern to convert
151    * @param addr
152    *          InetAddress of the host used for substitution
153    * @return converted Kerberos principal name
154    * @throws IOException if the client address cannot be determined
155    */
156   @InterfaceAudience.Public
157   @InterfaceStability.Evolving
getServerPrincipal(String principalConfig, InetAddress addr)158   public static String getServerPrincipal(String principalConfig,
159       InetAddress addr) throws IOException {
160     String[] components = getComponents(principalConfig);
161     if (components == null || components.length != 3
162         || !components[1].equals(HOSTNAME_PATTERN)) {
163       return principalConfig;
164     } else {
165       if (addr == null) {
166         throw new IOException("Can't replace " + HOSTNAME_PATTERN
167             + " pattern since client address is null");
168       }
169       return replacePattern(components, addr.getCanonicalHostName());
170     }
171   }
172 
getComponents(String principalConfig)173   private static String[] getComponents(String principalConfig) {
174     if (principalConfig == null)
175       return null;
176     return principalConfig.split("[/@]");
177   }
178 
replacePattern(String[] components, String hostname)179   private static String replacePattern(String[] components, String hostname)
180       throws IOException {
181     String fqdn = hostname;
182     if (fqdn == null || fqdn.isEmpty() || fqdn.equals("0.0.0.0")) {
183       fqdn = getLocalHostName();
184     }
185     return components[0] + "/" +
186         StringUtils.toLowerCase(fqdn) + "@" + components[2];
187   }
188 
getLocalHostName()189   static String getLocalHostName() throws UnknownHostException {
190     return InetAddress.getLocalHost().getCanonicalHostName();
191   }
192 
193   /**
194    * Login as a principal specified in config. Substitute $host in
195    * user's Kerberos principal name with a dynamically looked-up fully-qualified
196    * domain name of the current host.
197    *
198    * @param conf
199    *          conf to use
200    * @param keytabFileKey
201    *          the key to look for keytab file in conf
202    * @param userNameKey
203    *          the key to look for user's Kerberos principal name in conf
204    * @throws IOException if login fails
205    */
206   @InterfaceAudience.Public
207   @InterfaceStability.Evolving
login(final Configuration conf, final String keytabFileKey, final String userNameKey)208   public static void login(final Configuration conf,
209       final String keytabFileKey, final String userNameKey) throws IOException {
210     login(conf, keytabFileKey, userNameKey, getLocalHostName());
211   }
212 
213   /**
214    * Login as a principal specified in config. Substitute $host in user's Kerberos principal
215    * name with hostname. If non-secure mode - return. If no keytab available -
216    * bail out with an exception
217    *
218    * @param conf
219    *          conf to use
220    * @param keytabFileKey
221    *          the key to look for keytab file in conf
222    * @param userNameKey
223    *          the key to look for user's Kerberos principal name in conf
224    * @param hostname
225    *          hostname to use for substitution
226    * @throws IOException if the config doesn't specify a keytab
227    */
228   @InterfaceAudience.Public
229   @InterfaceStability.Evolving
login(final Configuration conf, final String keytabFileKey, final String userNameKey, String hostname)230   public static void login(final Configuration conf,
231       final String keytabFileKey, final String userNameKey, String hostname)
232       throws IOException {
233 
234     if(! UserGroupInformation.isSecurityEnabled())
235       return;
236 
237     String keytabFilename = conf.get(keytabFileKey);
238     if (keytabFilename == null || keytabFilename.length() == 0) {
239       throw new IOException("Running in secure mode, but config doesn't have a keytab");
240     }
241 
242     String principalConfig = conf.get(userNameKey, System
243         .getProperty("user.name"));
244     String principalName = SecurityUtil.getServerPrincipal(principalConfig,
245         hostname);
246     UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename);
247   }
248 
249   /**
250    * create the service name for a Delegation token
251    * @param uri of the service
252    * @param defPort is used if the uri lacks a port
253    * @return the token service, or null if no authority
254    * @see #buildTokenService(InetSocketAddress)
255    */
buildDTServiceName(URI uri, int defPort)256   public static String buildDTServiceName(URI uri, int defPort) {
257     String authority = uri.getAuthority();
258     if (authority == null) {
259       return null;
260     }
261     InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort);
262     return buildTokenService(addr).toString();
263    }
264 
265   /**
266    * Get the host name from the principal name of format <service>/host@realm.
267    * @param principalName principal name of format as described above
268    * @return host name if the the string conforms to the above format, else null
269    */
getHostFromPrincipal(String principalName)270   public static String getHostFromPrincipal(String principalName) {
271     return new HadoopKerberosName(principalName).getHostName();
272   }
273 
274   private static ServiceLoader<SecurityInfo> securityInfoProviders =
275     ServiceLoader.load(SecurityInfo.class);
276   private static SecurityInfo[] testProviders = new SecurityInfo[0];
277 
278   /**
279    * Test setup method to register additional providers.
280    * @param providers a list of high priority providers to use
281    */
282   @InterfaceAudience.Private
setSecurityInfoProviders(SecurityInfo... providers)283   public static void setSecurityInfoProviders(SecurityInfo... providers) {
284     testProviders = providers;
285   }
286 
287   /**
288    * Look up the KerberosInfo for a given protocol. It searches all known
289    * SecurityInfo providers.
290    * @param protocol the protocol class to get the information for
291    * @param conf configuration object
292    * @return the KerberosInfo or null if it has no KerberosInfo defined
293    */
294   public static KerberosInfo
getKerberosInfo(Class<?> protocol, Configuration conf)295   getKerberosInfo(Class<?> protocol, Configuration conf) {
296     for(SecurityInfo provider: testProviders) {
297       KerberosInfo result = provider.getKerberosInfo(protocol, conf);
298       if (result != null) {
299         return result;
300       }
301     }
302 
303     synchronized (securityInfoProviders) {
304       for(SecurityInfo provider: securityInfoProviders) {
305         KerberosInfo result = provider.getKerberosInfo(protocol, conf);
306         if (result != null) {
307           return result;
308         }
309       }
310     }
311     return null;
312   }
313 
314   /**
315    * Look up the TokenInfo for a given protocol. It searches all known
316    * SecurityInfo providers.
317    * @param protocol The protocol class to get the information for.
318    * @param conf Configuration object
319    * @return the TokenInfo or null if it has no KerberosInfo defined
320    */
getTokenInfo(Class<?> protocol, Configuration conf)321   public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) {
322     for(SecurityInfo provider: testProviders) {
323       TokenInfo result = provider.getTokenInfo(protocol, conf);
324       if (result != null) {
325         return result;
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    */
getTokenServiceAddr(Token<?> token)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    */
setTokenService(Token<?> token, InetSocketAddress addr)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    */
buildTokenService(InetSocketAddress addr)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 = StringUtils.toLowerCase(addr.getHostName());
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    */
buildTokenService(URI uri)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    */
doAsLoginUserOrFatal(PrivilegedAction<T> action)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    */
doAsLoginUser(PrivilegedExceptionAction<T> action)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    */
doAsCurrentUser(PrivilegedExceptionAction<T> action)440   public static <T> T doAsCurrentUser(PrivilegedExceptionAction<T> action)
441       throws IOException {
442     return doAsUser(UserGroupInformation.getCurrentUser(), action);
443   }
444 
doAsUser(UserGroupInformation ugi, PrivilegedExceptionAction<T> action)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
getByName(String hostname)464   InetAddress getByName(String hostname) throws UnknownHostException {
465     return hostResolver.getByName(hostname);
466   }
467 
468   interface HostResolver {
getByName(String host)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
getByName(String host)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
getByName(String host)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 
getByExactName(String host)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 
getByNameWithSearch(String host)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
getInetAddressByName(String host)598     InetAddress getInetAddressByName(String host) throws UnknownHostException {
599       return InetAddress.getByName(host);
600     }
601 
setSearchDomains(String .... domains)602     void setSearchDomains(String ... domains) {
603       searchDomains = Arrays.asList(domains);
604     }
605   }
606 
getAuthenticationMethod(Configuration conf)607   public static AuthenticationMethod getAuthenticationMethod(Configuration conf) {
608     String value = conf.get(HADOOP_SECURITY_AUTHENTICATION, "simple");
609     try {
610       return Enum.valueOf(AuthenticationMethod.class,
611           StringUtils.toUpperCase(value));
612     } catch (IllegalArgumentException iae) {
613       throw new IllegalArgumentException("Invalid attribute value for " +
614           HADOOP_SECURITY_AUTHENTICATION + " of " + value);
615     }
616   }
617 
setAuthenticationMethod( AuthenticationMethod authenticationMethod, Configuration conf)618   public static void setAuthenticationMethod(
619       AuthenticationMethod authenticationMethod, Configuration conf) {
620     if (authenticationMethod == null) {
621       authenticationMethod = AuthenticationMethod.SIMPLE;
622     }
623     conf.set(HADOOP_SECURITY_AUTHENTICATION,
624         StringUtils.toLowerCase(authenticationMethod.toString()));
625   }
626 
627   /*
628    * Check if a given port is privileged.
629    * The ports with number smaller than 1024 are treated as privileged ports in
630    * unix/linux system. For other operating systems, use this method with care.
631    * For example, Windows doesn't have the concept of privileged ports.
632    * However, it may be used at Windows client to check port of linux server.
633    *
634    * @param port the port number
635    * @return true for privileged ports, false otherwise
636    *
637    */
isPrivilegedPort(final int port)638   public static boolean isPrivilegedPort(final int port) {
639     return port < 1024;
640   }
641 }
642