/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF * licenses this file to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.apache.hadoop.security; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.security.AccessController; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosTicket; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.io.Text; import org.apache.hadoop.net.NetUtils; import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.apache.hadoop.security.authorize.AccessControlList; import org.apache.hadoop.security.token.Token; //this will need to be replaced someday when there is a suitable replacement import sun.net.dns.ResolverConfiguration; import sun.net.util.IPAddressUtil; public class SecurityUtil { public static final Log LOG = LogFactory.getLog(SecurityUtil.class); public static final String HOSTNAME_PATTERN = "_HOST"; // controls whether buildTokenService will use an ip or host/ip as given // by the user; visible for testing static boolean useIpForTokenService; static HostResolver hostResolver; private static final boolean useKsslAuth; static { Configuration conf = new Configuration(); boolean useIp = conf.getBoolean( CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP, CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT); setTokenServiceUseIp(useIp); useKsslAuth = conf.getBoolean( CommonConfigurationKeys.HADOOP_SECURITY_USE_WEAK_HTTP_CRYPTO_KEY, CommonConfigurationKeys.HADOOP_SECURITY_USE_WEAK_HTTP_CRYPTO_DEFAULT); } /** * For use only by tests! */ static void setTokenServiceUseIp(boolean flag) { useIpForTokenService = flag; hostResolver = !useIpForTokenService ? new QualifiedHostResolver() : new StandardHostResolver(); } /** * Find the original TGT within the current subject's credentials. Cross-realm * TGT's of the form "krbtgt/TWO.COM@ONE.COM" may be present. * * @return The TGT from the current subject * @throws IOException * if TGT can't be found */ private static KerberosTicket getTgtFromSubject() throws IOException { Subject current = Subject.getSubject(AccessController.getContext()); if (current == null) { throw new IOException( "Can't get TGT from current Subject, because it is null"); } Set<KerberosTicket> tickets = current .getPrivateCredentials(KerberosTicket.class); for (KerberosTicket t : tickets) { if (isOriginalTGT(t.getServer().getName())) return t; } throw new IOException("Failed to find TGT from current Subject:"+current); } // Original TGT must be of form "krbtgt/FOO@FOO". Verify this protected static boolean isOriginalTGT(String name) { if(name == null) return false; String [] components = name.split("[/@]"); return components.length == 3 && "krbtgt".equals(components[0]) && components[1].equals(components[2]); } /** * Explicitly pull the service ticket for the specified host. This solves a * problem with Java's Kerberos SSL problem where the client cannot * authenticate against a cross-realm service. It is necessary for clients * making kerberized https requests to call this method on the target URL * to ensure that in a cross-realm environment the remote host will be * successfully authenticated. * * This method is internal to Hadoop and should not be used by other * applications. This method should not be considered stable or open: * it will be removed when the Java behavior is changed. * * @param remoteHost Target URL the krb-https client will access * @throws IOException if a service ticket is not available */ public static void fetchServiceTicket(URL remoteHost) throws IOException { if(!UserGroupInformation.isSecurityEnabled()) return; String serviceName = KerberosUtil.getServicePrincipal("host", remoteHost.getHost()); if (LOG.isDebugEnabled()) LOG.debug("Fetching service ticket for host at: " + serviceName); Object serviceCred = null; Method credsToTicketMeth; Class<?> krb5utilClass; try { Class<?> principalClass; Class<?> credentialsClass; if (System.getProperty("java.vendor").contains("IBM")) { principalClass = Class.forName("com.ibm.security.krb5.PrincipalName"); credentialsClass = Class.forName("com.ibm.security.krb5.Credentials"); krb5utilClass = Class.forName("com.ibm.security.jgss.mech.krb5"); } else { principalClass = Class.forName("sun.security.krb5.PrincipalName"); credentialsClass = Class.forName("sun.security.krb5.Credentials"); krb5utilClass = Class.forName("sun.security.jgss.krb5.Krb5Util"); } @SuppressWarnings("rawtypes") Constructor principalConstructor = principalClass.getConstructor(String.class, int.class); Field KRB_NT_SRV_HST = principalClass.getDeclaredField("KRB_NT_SRV_HST"); Method acquireServiceCredsMeth = credentialsClass.getDeclaredMethod("acquireServiceCreds", String.class, credentialsClass); Method ticketToCredsMeth = krb5utilClass.getDeclaredMethod("ticketToCreds", KerberosTicket.class); credsToTicketMeth = krb5utilClass.getDeclaredMethod("credsToTicket", credentialsClass); Object principal = principalConstructor.newInstance(serviceName, KRB_NT_SRV_HST.get(principalClass)); serviceCred = acquireServiceCredsMeth.invoke(credentialsClass, principal.toString(), ticketToCredsMeth.invoke(krb5utilClass, getTgtFromSubject())); } catch (Exception e) { throw new IOException("Can't get service ticket for: " + serviceName, e); } if (serviceCred == null) { throw new IOException("Can't get service ticket for " + serviceName); } try { Subject.getSubject(AccessController.getContext()).getPrivateCredentials() .add(credsToTicketMeth.invoke(krb5utilClass, serviceCred)); } catch (Exception e) { throw new IOException("Can't get service ticket for: " + serviceName, e); } } /** * Convert Kerberos principal name pattern to valid Kerberos principal * names. It replaces hostname pattern with hostname, which should be * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses * dynamically looked-up fqdn of the current host instead. * * @param principalConfig * the Kerberos principal name conf value to convert * @param hostname * the fully-qualified domain name used for substitution * @return converted Kerberos principal name * @throws IOException if the service ticket cannot be retrieved */ public static String getServerPrincipal(String principalConfig, String hostname) throws IOException { String[] components = getComponents(principalConfig); if (components == null || components.length != 3 || !components[1].equals(HOSTNAME_PATTERN)) { return principalConfig; } else { return replacePattern(components, hostname); } } /** * Convert Kerberos principal name pattern to valid Kerberos principal names. * This method is similar to {@link #getServerPrincipal(String, String)}, * except 1) the reverse DNS lookup from addr to hostname is done only when * necessary, 2) param addr can't be null (no default behavior of using local * hostname when addr is null). * * @param principalConfig * Kerberos principal name pattern to convert * @param addr * InetAddress of the host used for substitution * @return converted Kerberos principal name * @throws IOException if the client address cannot be determined */ public static String getServerPrincipal(String principalConfig, InetAddress addr) throws IOException { String[] components = getComponents(principalConfig); if (components == null || components.length != 3 || !components[1].equals(HOSTNAME_PATTERN)) { return principalConfig; } else { if (addr == null) { throw new IOException("Can't replace " + HOSTNAME_PATTERN + " pattern since client address is null"); } return replacePattern(components, addr.getCanonicalHostName()); } } private static String[] getComponents(String principalConfig) { if (principalConfig == null) return null; return principalConfig.split("[/@]"); } private static String replacePattern(String[] components, String hostname) throws IOException { String fqdn = hostname; if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) { fqdn = getLocalHostName(); } return components[0] + "/" + fqdn.toLowerCase(Locale.US) + "@" + components[2]; } /** * Get the fqdn for the current host. * * @return fqdn of the current host. * @throws UnknownHostException * if no IP address for the local host could be found. */ public static String getLocalHostName() throws UnknownHostException { return InetAddress.getLocalHost().getCanonicalHostName(); } /** * If a keytab has been provided, login as that user. Substitute $host in * user's Kerberos principal name with a dynamically looked-up fully-qualified * domain name of the current host. * * @param conf * conf to use * @param keytabFileKey * the key to look for keytab file in conf * @param userNameKey * the key to look for user's Kerberos principal name in conf * @throws IOException if the client address cannot be determined */ public static void login(final Configuration conf, final String keytabFileKey, final String userNameKey) throws IOException { login(conf, keytabFileKey, userNameKey, getLocalHostName()); } /** * If a keytab has been provided, login as that user. Substitute $host in * user's Kerberos principal name with hostname. * * @param conf * conf to use * @param keytabFileKey * the key to look for keytab file in conf * @param userNameKey * the key to look for user's Kerberos principal name in conf * @param hostname * hostname to use for substitution * @throws IOException if login fails */ public static void login(final Configuration conf, final String keytabFileKey, final String userNameKey, String hostname) throws IOException { String keytabFilename = conf.get(keytabFileKey); if (keytabFilename == null) return; String principalConfig = conf.get(userNameKey, System .getProperty("user.name")); String principalName = SecurityUtil.getServerPrincipal(principalConfig, hostname); UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename); } /** * Decode the given token's service field into an InetAddress * @param token from which to obtain the service * @return InetAddress for the service */ public static InetSocketAddress getTokenServiceAddr(Token<?> token) { return NetUtils.createSocketAddr(token.getService().toString()); } /** * Set the given token's service to the format expected by the RPC client * @param token a delegation token * @param addr the socket for the rpc connection */ public static void setTokenService(Token<?> token, InetSocketAddress addr) { token.setService(buildTokenService(addr)); } /** * Construct the service key for a token * @param addr InetSocketAddress of remote connection with a token * @return "ip:port" or "host:port" depending on the value of * hadoop.security.token.service.use_ip */ public static Text buildTokenService(InetSocketAddress addr) { String host = null; if (useIpForTokenService) { if (addr.isUnresolved()) { // host has no ip address throw new IllegalArgumentException( new UnknownHostException(addr.getHostName()) ); } host = addr.getAddress().getHostAddress(); } else { host = addr.getHostName().toLowerCase(); } return new Text(host + ":" + addr.getPort()); } /** * create the service name for a Delegation token * @param uri of the service * @param defPort is used if the uri lacks a port * @return the token service, or null if no authority * @see #buildTokenService(InetSocketAddress) */ public static String buildDTServiceName(URI uri, int defPort) { String authority = uri.getAuthority(); if (authority == null || authority.isEmpty()) { return null; } InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort); return buildTokenService(addr).toString(); } /** * Get the ACL object representing the cluster administrators * The user who starts the daemon is automatically added as an admin * @param conf * @param configKey the key that holds the ACL string in its value * @return AccessControlList instance */ public static AccessControlList getAdminAcls(Configuration conf, String configKey) { try { AccessControlList adminAcl = new AccessControlList(conf.get(configKey, " ")); adminAcl.addUser(UserGroupInformation.getCurrentUser(). getShortUserName()); return adminAcl; } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Get the host name from the principal name of format <service>/host@realm. * @param principalName principal name of format as described above * @return host name if the the string conforms to the above format, else null */ public static String getHostFromPrincipal(String principalName) { return new KerberosName(principalName).getHostName(); } /** * @return true if we should use KSSL to authenticate NN HTTP endpoints, * false to use SPNEGO or if security is disabled. */ public static boolean useKsslAuth() { return UserGroupInformation.isSecurityEnabled() && useKsslAuth; } /** * Open a (if need be) secure connection to a URL in a secure environment * that is using SPNEGO or KSSL to authenticate its URLs. All Namenode and * Secondary Namenode URLs that are protected via SPNEGO or KSSL should be * accessed via this method. * * @param url to authenticate via SPNEGO. * @return A connection that has been authenticated via SPNEGO * @throws IOException If unable to authenticate via SPNEGO */ public static URLConnection openSecureHttpConnection(URL url) throws IOException { if (useKsslAuth) { // Avoid Krb bug with cross-realm hosts fetchServiceTicket(url); } if (!UserGroupInformation.isSecurityEnabled() || useKsslAuth) { return url.openConnection(); } else { AuthenticatedURL.Token token = new AuthenticatedURL.Token(); try { return new AuthenticatedURL().openConnection(url, token); } catch (AuthenticationException e) { throw new IOException("Exception trying to open authenticated connection to " + url, e); } } } /** * Resolves a host subject to the security requirements determined by * hadoop.security.token.service.use_ip. * * @param hostname host or ip to resolve * @return a resolved host * @throws UnknownHostException if the host doesn't exist */ //@InterfaceAudience.Private public static InetAddress getByName(String hostname) throws UnknownHostException { return hostResolver.getByName(hostname); } interface HostResolver { InetAddress getByName(String host) throws UnknownHostException; } /** * Uses standard java host resolution */ static class StandardHostResolver implements HostResolver { public InetAddress getByName(String host) throws UnknownHostException { return InetAddress.getByName(host); } } /** * This an alternate resolver with important properties that the standard * java resolver lacks: * 1) The hostname is fully qualified. This avoids security issues if not * all hosts in the cluster do not share the same search domains. It * also prevents other hosts from performing unnecessary dns searches. * In contrast, InetAddress simply returns the host as given. * 2) The InetAddress is instantiated with an exact host and IP to prevent * further unnecessary lookups. InetAddress may perform an unnecessary * reverse lookup for an IP. * 3) A call to getHostName() will always return the qualified hostname, or * more importantly, the IP if instantiated with an IP. This avoids * unnecessary dns timeouts if the host is not resolvable. * 4) Point 3 also ensures that if the host is re-resolved, ex. during a * connection re-attempt, that a reverse lookup to host and forward * lookup to IP is not performed since the reverse/forward mappings may * not always return the same IP. If the client initiated a connection * with an IP, then that IP is all that should ever be contacted. * * NOTE: this resolver is only used if: * hadoop.security.token.service.use_ip=false */ protected static class QualifiedHostResolver implements HostResolver { @SuppressWarnings("unchecked") private List<String> searchDomains = ResolverConfiguration.open().searchlist(); /** * Create an InetAddress with a fully qualified hostname of the given * hostname. InetAddress does not qualify an incomplete hostname that * is resolved via the domain search list. * {@link InetAddress#getCanonicalHostName()} will fully qualify the * hostname, but it always return the A record whereas the given hostname * may be a CNAME. * * @param host a hostname or ip address * @return InetAddress with the fully qualified hostname or ip * @throws UnknownHostException if host does not exist */ public InetAddress getByName(String host) throws UnknownHostException { InetAddress addr = null; if (IPAddressUtil.isIPv4LiteralAddress(host)) { // use ipv4 address as-is byte[] ip = IPAddressUtil.textToNumericFormatV4(host); addr = InetAddress.getByAddress(host, ip); } else if (IPAddressUtil.isIPv6LiteralAddress(host)) { // use ipv6 address as-is byte[] ip = IPAddressUtil.textToNumericFormatV6(host); addr = InetAddress.getByAddress(host, ip); } else if (host.endsWith(".")) { // a rooted host ends with a dot, ex. "host." // rooted hosts never use the search path, so only try an exact lookup addr = getByExactName(host); } else if (host.contains(".")) { // the host contains a dot (domain), ex. "host.domain" // try an exact host lookup, then fallback to search list addr = getByExactName(host); if (addr == null) { addr = getByNameWithSearch(host); } } else { // it's a simple host with no dots, ex. "host" // try the search list, then fallback to exact host InetAddress loopback = InetAddress.getByName(null); if (host.equalsIgnoreCase(loopback.getHostName())) { addr = InetAddress.getByAddress(host, loopback.getAddress()); } else { addr = getByNameWithSearch(host); if (addr == null) { addr = getByExactName(host); } } } // unresolvable! if (addr == null) { throw new UnknownHostException(host); } return addr; } InetAddress getByExactName(String host) { InetAddress addr = null; // InetAddress will use the search list unless the host is rooted // with a trailing dot. The trailing dot will disable any use of the // search path in a lower level resolver. See RFC 1535. String fqHost = host; if (!fqHost.endsWith(".")) fqHost += "."; try { addr = getInetAddressByName(fqHost); // can't leave the hostname as rooted or other parts of the system // malfunction, ex. kerberos principals are lacking proper host // equivalence for rooted/non-rooted hostnames addr = InetAddress.getByAddress(host, addr.getAddress()); } catch (UnknownHostException e) { // ignore, caller will throw if necessary } return addr; } InetAddress getByNameWithSearch(String host) { InetAddress addr = null; if (host.endsWith(".")) { // already qualified? addr = getByExactName(host); } else { for (String domain : searchDomains) { String dot = !domain.startsWith(".") ? "." : ""; addr = getByExactName(host + dot + domain); if (addr != null) break; } } return addr; } // implemented as a separate method to facilitate unit testing InetAddress getInetAddressByName(String host) throws UnknownHostException { return InetAddress.getByName(host); } void setSearchDomains(String ... domains) { searchDomains = Arrays.asList(domains); } } }