blob: 4b2dff20351964555cb26ca0fddc168dcb6ac7f3 [file] [log] [blame]
/**
* 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.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.AccessController;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.net.NetUtils;
import sun.security.jgss.krb5.Krb5Util;
import sun.security.krb5.Credentials;
import sun.security.krb5.PrincipalName;
@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
@InterfaceStability.Evolving
public class SecurityUtil {
public static final Log LOG = LogFactory.getLog(SecurityUtil.class);
public static final String HOSTNAME_PATTERN = "_HOST";
/**
* 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))
return t;
}
throw new IOException("Failed to find TGT from current Subject");
}
/**
* TGS must have the server principal of the form "krbtgt/FOO@FOO".
* @param principal
* @return true or false
*/
static boolean
isTGSPrincipal(KerberosPrincipal principal) {
if (principal == null)
return false;
if (principal.getName().equals("krbtgt/" + principal.getRealm() +
"@" + principal.getRealm())) {
return true;
}
return false;
}
/**
* Check whether the server principal is the TGS's principal
* @param ticket the original TGT (the ticket that is obtained when a
* kinit is done)
* @return true or false
*/
protected static boolean isOriginalTGT(KerberosTicket ticket) {
return isTGSPrincipal(ticket.getServer());
}
/**
* 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
*/
public static void fetchServiceTicket(URL remoteHost) throws IOException {
if(!UserGroupInformation.isSecurityEnabled())
return;
String serviceName = "host/" + remoteHost.getHost();
if (LOG.isDebugEnabled())
LOG.debug("Fetching service ticket for host at: " + serviceName);
Credentials serviceCred = null;
try {
PrincipalName principal = new PrincipalName(serviceName,
PrincipalName.KRB_NT_SRV_HST);
serviceCred = Credentials.acquireServiceCreds(principal
.toString(), Krb5Util.ticketToCreds(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);
}
Subject.getSubject(AccessController.getContext()).getPrivateCredentials()
.add(Krb5Util.credsToTicket(serviceCred));
}
/**
* 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
*/
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
*/
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 + "@" + components[2];
}
static String getLocalHostName() throws UnknownHostException {
return InetAddress.getLocalHost().getCanonicalHostName();
}
/**
* Login as a principal specified in config. 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
*/
public static void login(final Configuration conf,
final String keytabFileKey, final String userNameKey) throws IOException {
login(conf, keytabFileKey, userNameKey, getLocalHostName());
}
/**
* Login as a principal specified in config. Substitute $host in user's Kerberos principal
* name with hostname. If non-secure mode - return. If no keytab available -
* bail out with an exception
*
* @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
*/
public static void login(final Configuration conf,
final String keytabFileKey, final String userNameKey, String hostname)
throws IOException {
if(! UserGroupInformation.isSecurityEnabled())
return;
String keytabFilename = conf.get(keytabFileKey);
if (keytabFilename == null || keytabFilename.length() == 0) {
throw new IOException("Running in secure mode, but config doesn't have a keytab");
}
String principalConfig = conf.get(userNameKey, System
.getProperty("user.name"));
String principalName = SecurityUtil.getServerPrincipal(principalConfig,
hostname);
UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename);
}
/**
* create service name for Delegation token ip:port
* @param uri
* @param defPort
* @return "ip:port"
*/
public static String buildDTServiceName(URI uri, int defPort) {
int port = uri.getPort();
if(port == -1)
port = defPort;
// build the service name string "/ip:port"
// for whatever reason using NetUtils.createSocketAddr(target).toString()
// returns "localhost/ip:port"
StringBuffer sb = new StringBuffer();
String host = uri.getHost();
if (host != null) {
host = NetUtils.normalizeHostName(host);
} else {
host = "";
}
sb.append(host).append(":").append(port);
return sb.toString();
}
}