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