HADOOP-6632. Adds support for using different keytabs for different servers in a Hadoop cluster. In the earier implementation, all servers of a certain type \(like TaskTracker\), would have the same keytab and the same principal. Now the principal name is a pattern that has _HOST in it. Contributed by Kan Zhang & Jitendra Pandey.

git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@965696 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Devaraj Das 2010-07-20 00:46:19 +00:00
parent 75e78e0484
commit fa3a3bf5d8
10 changed files with 194 additions and 21 deletions

View File

@ -74,6 +74,12 @@ Trunk (unreleased changes)
HADOOP-6905. add buildDTServiceName method to SecurityUtil HADOOP-6905. add buildDTServiceName method to SecurityUtil
(as part of MAPREDUCE-1718) (boryas) (as part of MAPREDUCE-1718) (boryas)
HADOOP-6632. Adds support for using different keytabs for different
servers in a Hadoop cluster. In the earier implementation, all servers
of a certain type (like TaskTracker), would have the same keytab and the
same principal. Now the principal name is a pattern that has _HOST in it.
(Kan Zhang & Jitendra Pandey via ddas)
OPTIMIZATIONS OPTIMIZATIONS
BUG FIXES BUG FIXES

View File

@ -54,6 +54,7 @@
import org.apache.hadoop.security.KerberosInfo; import org.apache.hadoop.security.KerberosInfo;
import org.apache.hadoop.security.SaslRpcClient; import org.apache.hadoop.security.SaslRpcClient;
import org.apache.hadoop.security.SaslRpcServer.AuthMethod; import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.security.token.TokenIdentifier;
@ -254,13 +255,15 @@ public Connection(ConnectionId remoteId) throws IOException {
KerberosInfo krbInfo = protocol.getAnnotation(KerberosInfo.class); KerberosInfo krbInfo = protocol.getAnnotation(KerberosInfo.class);
if (krbInfo != null) { if (krbInfo != null) {
String serverKey = krbInfo.serverPrincipal(); String serverKey = krbInfo.serverPrincipal();
if (serverKey != null) { if (serverKey == null) {
if(LOG.isDebugEnabled()) { throw new IOException(
LOG.info("server principal key for protocol=" "Can't obtain server Kerberos config key from KerberosInfo");
+ protocol.getCanonicalName() + " is " + serverKey +
" and val =" + conf.get(serverKey));
} }
serverPrincipal = conf.get(serverKey); serverPrincipal = SecurityUtil.getServerPrincipal(
conf.get(serverKey), server.getAddress().getCanonicalHostName());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for protocol="
+ protocol.getCanonicalName() + " is " + serverPrincipal);
} }
} }
} }

View File

@ -827,6 +827,7 @@ public class Connection {
// Cache the remote host & port info so that even if the socket is // Cache the remote host & port info so that even if the socket is
// disconnected, we can say where it used to connect to. // disconnected, we can say where it used to connect to.
private String hostAddress; private String hostAddress;
private String hostName;
private int remotePort; private int remotePort;
ConnectionHeader header = new ConnectionHeader(); ConnectionHeader header = new ConnectionHeader();
@ -869,6 +870,7 @@ public Connection(SelectionKey key, SocketChannel channel,
this.hostAddress = "*Unknown*"; this.hostAddress = "*Unknown*";
} else { } else {
this.hostAddress = addr.getHostAddress(); this.hostAddress = addr.getHostAddress();
this.hostName = addr.getCanonicalHostName();
} }
this.remotePort = socket.getPort(); this.remotePort = socket.getPort();
this.responseQueue = new LinkedList<Call>(); this.responseQueue = new LinkedList<Call>();
@ -891,6 +893,10 @@ public String getHostAddress() {
return hostAddress; return hostAddress;
} }
public String getHostName() {
return hostName;
}
public void setLastContact(long lastContact) { public void setLastContact(long lastContact) {
this.lastContact = lastContact; this.lastContact = lastContact;
} }
@ -1296,7 +1302,7 @@ private boolean authorizeConnection() throws IOException {
&& (authMethod != AuthMethod.DIGEST)) { && (authMethod != AuthMethod.DIGEST)) {
ProxyUsers.authorize(user, this.getHostAddress(), conf); ProxyUsers.authorize(user, this.getHostAddress(), conf);
} }
authorize(user, header); authorize(user, header, getHostName());
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Successfully authorized " + header); LOG.debug("Successfully authorized " + header);
} }
@ -1626,10 +1632,12 @@ public abstract Writable call(Class<?> protocol,
* *
* @param user client user * @param user client user
* @param connection incoming connection * @param connection incoming connection
* @param hostname fully-qualified domain name of incoming connection
* @throws AuthorizationException when the client isn't authorized to talk the protocol * @throws AuthorizationException when the client isn't authorized to talk the protocol
*/ */
public void authorize(UserGroupInformation user, public void authorize(UserGroupInformation user,
ConnectionHeader connection ConnectionHeader connection,
String hostname
) throws AuthorizationException { ) throws AuthorizationException {
if (authorize) { if (authorize) {
Class<?> protocol = null; Class<?> protocol = null;
@ -1639,7 +1647,7 @@ public void authorize(UserGroupInformation user,
throw new AuthorizationException("Unknown protocol: " + throw new AuthorizationException("Unknown protocol: " +
connection.getProtocol()); connection.getProtocol());
} }
ServiceAuthorizationManager.authorize(user, protocol, getConf()); ServiceAuthorizationManager.authorize(user, protocol, getConf(), hostname);
} }
} }

View File

@ -17,8 +17,10 @@
package org.apache.hadoop.security; package org.apache.hadoop.security;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException;
import java.security.AccessController; import java.security.AccessController;
import java.util.Set; import java.util.Set;
@ -29,6 +31,8 @@
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability; 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 org.apache.hadoop.net.NetUtils;
import sun.security.jgss.krb5.Krb5Util; import sun.security.jgss.krb5.Krb5Util;
@ -38,7 +42,8 @@
@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
@InterfaceStability.Evolving @InterfaceStability.Evolving
public class SecurityUtil { public class SecurityUtil {
private static final Log LOG = LogFactory.getLog(SecurityUtil.class); 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 * Find the original TGT within the current subject's credentials. Cross-realm
@ -49,9 +54,13 @@ public class SecurityUtil {
* if TGT can't be found * if TGT can't be found
*/ */
private static KerberosTicket getTgtFromSubject() throws IOException { private static KerberosTicket getTgtFromSubject() throws IOException {
Set<KerberosTicket> tickets = Subject.getSubject( Subject current = Subject.getSubject(AccessController.getContext());
AccessController.getContext()).getPrivateCredentials( if (current == null) {
KerberosTicket.class); 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) { for (KerberosTicket t : tickets) {
if (isOriginalTGT(t.getServer().getName())) if (isOriginalTGT(t.getServer().getName()))
return t; return t;
@ -90,6 +99,7 @@ public static void fetchServiceTicket(URL remoteHost) throws IOException {
return; return;
String serviceName = "host/" + remoteHost.getHost(); String serviceName = "host/" + remoteHost.getHost();
if (LOG.isDebugEnabled())
LOG.debug("Fetching service ticket for host at: " + serviceName); LOG.debug("Fetching service ticket for host at: " + serviceName);
Credentials serviceCred = null; Credentials serviceCred = null;
try { try {
@ -98,7 +108,7 @@ public static void fetchServiceTicket(URL remoteHost) throws IOException {
serviceCred = Credentials.acquireServiceCreds(principal serviceCred = Credentials.acquireServiceCreds(principal
.toString(), Krb5Util.ticketToCreds(getTgtFromSubject())); .toString(), Krb5Util.ticketToCreds(getTgtFromSubject()));
} catch (Exception e) { } catch (Exception e) {
throw new IOException("Invalid service principal name: " throw new IOException("Can't get service ticket for: "
+ serviceName, e); + serviceName, e);
} }
if (serviceCred == null) { if (serviceCred == null) {
@ -108,6 +118,91 @@ public static void fetchServiceTicket(URL remoteHost) throws IOException {
.add(Krb5Util.credsToTicket(serviceCred)); .add(Krb5Util.credsToTicket(serviceCred));
} }
/**
* Convert Kerberos principal name conf values to valid Kerberos principal
* names. It replaces $host in the conf values 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 {
if (principalConfig == null)
return null;
String[] components = principalConfig.split("[/@]");
if (components.length != 3) {
throw new IOException(
"Kerberos service principal name isn't configured properly "
+ "(should have 3 parts): " + principalConfig);
}
if (components[1].equals(HOSTNAME_PATTERN)) {
String fqdn = hostname;
if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) {
fqdn = getLocalHostName();
}
return components[0] + "/" + fqdn + "@" + components[2];
} else {
return principalConfig;
}
}
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
*/
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
*/
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);
}
/** /**
* create service name for Delegation token ip:port * create service name for Delegation token ip:port
* @param uri * @param uri

View File

@ -437,6 +437,8 @@ static void loginUserFromKeytab(String user,
throw new IOException("Login failure for " + user + " from keytab " + throw new IOException("Login failure for " + user + " from keytab " +
path, le); path, le);
} }
LOG.info("Login successful for user " + keytabPrincipal
+ " using keytab file " + keytabFile);
} }
/** /**

View File

@ -28,6 +28,7 @@
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.security.KerberosInfo; import org.apache.hadoop.security.KerberosInfo;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.KerberosName; import org.apache.hadoop.security.KerberosName;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
@ -68,11 +69,14 @@ public class ServiceAuthorizationManager {
* *
* @param user user accessing the service * @param user user accessing the service
* @param protocol service being accessed * @param protocol service being accessed
* @param conf configuration to use
* @param hostname fully qualified domain name of the client
* @throws AuthorizationException on authorization failure * @throws AuthorizationException on authorization failure
*/ */
public static void authorize(UserGroupInformation user, public static void authorize(UserGroupInformation user,
Class<?> protocol, Class<?> protocol,
Configuration conf Configuration conf,
String hostname
) throws AuthorizationException { ) throws AuthorizationException {
AccessControlList acl = protocolToAcl.get(protocol); AccessControlList acl = protocolToAcl.get(protocol);
if (acl == null) { if (acl == null) {
@ -86,7 +90,19 @@ public static void authorize(UserGroupInformation user,
if (krbInfo != null) { if (krbInfo != null) {
String clientKey = krbInfo.clientPrincipal(); String clientKey = krbInfo.clientPrincipal();
if (clientKey != null && !clientKey.equals("")) { if (clientKey != null && !clientKey.equals("")) {
clientPrincipal = conf.get(clientKey); if (hostname == null) {
throw new AuthorizationException(
"Can't authorize client when client hostname is null");
}
try {
clientPrincipal = SecurityUtil.getServerPrincipal(
conf.get(clientKey), hostname);
} catch (IOException e) {
throw (AuthorizationException) new AuthorizationException(
"Can't figure out Kerberos principal name for connection from "
+ hostname + " for user=" + user + " protocol=" + protocol)
.initCause(e);
}
} }
} }
// when authorizing use the short name only // when authorizing use the short name only

View File

@ -27,6 +27,7 @@
import org.apache.hadoop.io.Text; import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableUtils; import org.apache.hadoop.io.WritableUtils;
import org.apache.hadoop.security.KerberosName;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.security.token.TokenIdentifier;
@ -57,7 +58,12 @@ public AbstractDelegationTokenIdentifier(Text owner, Text renewer, Text realUser
if (renewer == null) { if (renewer == null) {
this.renewer = new Text(); this.renewer = new Text();
} else { } else {
this.renewer = renewer; KerberosName renewerKrbName = new KerberosName(renewer.toString());
try {
this.renewer = new Text(renewerKrbName.getShortName());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
if (realUser == null) { if (realUser == null) {
this.realUser = new Text(); this.realUser = new Text();

View File

@ -35,6 +35,7 @@
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.KerberosName;
import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.SecretManager; import org.apache.hadoop.security.token.SecretManager;
import org.apache.hadoop.util.Daemon; import org.apache.hadoop.util.Daemon;
@ -280,8 +281,10 @@ public synchronized TokenIdent cancelToken(Token<TokenIdent> token,
} }
String owner = id.getUser().getUserName(); String owner = id.getUser().getUserName();
Text renewer = id.getRenewer(); Text renewer = id.getRenewer();
KerberosName cancelerKrbName = new KerberosName(canceller);
String cancelerShortName = cancelerKrbName.getShortName();
if (!canceller.equals(owner) if (!canceller.equals(owner)
&& (renewer == null || "".equals(renewer.toString()) || !canceller && (renewer == null || "".equals(renewer.toString()) || !cancelerShortName
.equals(renewer.toString()))) { .equals(renewer.toString()))) {
throw new AccessControlException(canceller throw new AccessControlException(canceller
+ " is not authorized to cancel the token"); + " is not authorized to cancel the token");

View File

@ -48,6 +48,7 @@
import org.apache.hadoop.security.SaslInputStream; import org.apache.hadoop.security.SaslInputStream;
import org.apache.hadoop.security.SaslRpcClient; import org.apache.hadoop.security.SaslRpcClient;
import org.apache.hadoop.security.SaslRpcServer; import org.apache.hadoop.security.SaslRpcServer;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
@ -63,6 +64,7 @@ public class TestSaslRPC {
static final String ERROR_MESSAGE = "Token is invalid"; static final String ERROR_MESSAGE = "Token is invalid";
static final String SERVER_PRINCIPAL_KEY = "test.ipc.server.principal"; static final String SERVER_PRINCIPAL_KEY = "test.ipc.server.principal";
static final String SERVER_KEYTAB_KEY = "test.ipc.server.keytab";
private static Configuration conf; private static Configuration conf;
static { static {
conf = new Configuration(); conf = new Configuration();
@ -76,6 +78,7 @@ public class TestSaslRPC {
((Log4JLogger) SaslRpcClient.LOG).getLogger().setLevel(Level.ALL); ((Log4JLogger) SaslRpcClient.LOG).getLogger().setLevel(Level.ALL);
((Log4JLogger) SaslRpcServer.LOG).getLogger().setLevel(Level.ALL); ((Log4JLogger) SaslRpcServer.LOG).getLogger().setLevel(Level.ALL);
((Log4JLogger) SaslInputStream.LOG).getLogger().setLevel(Level.ALL); ((Log4JLogger) SaslInputStream.LOG).getLogger().setLevel(Level.ALL);
((Log4JLogger) SecurityUtil.LOG).getLogger().setLevel(Level.ALL);
} }
public static class TestTokenIdentifier extends TokenIdentifier { public static class TestTokenIdentifier extends TokenIdentifier {
@ -248,7 +251,8 @@ private void doDigestRpc(Server server, TestTokenSecretManager sm)
static void testKerberosRpc(String principal, String keytab) throws Exception { static void testKerberosRpc(String principal, String keytab) throws Exception {
final Configuration newConf = new Configuration(conf); final Configuration newConf = new Configuration(conf);
newConf.set(SERVER_PRINCIPAL_KEY, principal); newConf.set(SERVER_PRINCIPAL_KEY, principal);
UserGroupInformation.loginUserFromKeytab(principal, keytab); newConf.set(SERVER_KEYTAB_KEY, keytab);
SecurityUtil.login(newConf, SERVER_KEYTAB_KEY, SERVER_PRINCIPAL_KEY);
UserGroupInformation current = UserGroupInformation.getCurrentUser(); UserGroupInformation current = UserGroupInformation.getCurrentUser();
System.out.println("UGI: " + current); System.out.println("UGI: " + current);
@ -269,6 +273,7 @@ static void testKerberosRpc(String principal, String keytab) throws Exception {
RPC.stopProxy(proxy); RPC.stopProxy(proxy);
} }
} }
System.out.println("Test is successful.");
} }
@Test @Test

View File

@ -17,6 +17,9 @@
package org.apache.hadoop.security; package org.apache.hadoop.security;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.IOException;
import org.junit.Test; import org.junit.Test;
public class TestSecurityUtil { public class TestSecurityUtil {
@ -32,4 +35,30 @@ public void isOriginalTGTReturnsCorrectValues() {
assertFalse(SecurityUtil.isOriginalTGT("this@is/notright")); assertFalse(SecurityUtil.isOriginalTGT("this@is/notright"));
assertFalse(SecurityUtil.isOriginalTGT("krbtgt/foo@FOO")); assertFalse(SecurityUtil.isOriginalTGT("krbtgt/foo@FOO"));
} }
private void verify(String original, String hostname, String expected)
throws IOException {
assertTrue(SecurityUtil.getServerPrincipal(original, hostname).equals(
expected));
assertTrue(SecurityUtil.getServerPrincipal(original, null).equals(
expected));
assertTrue(SecurityUtil.getServerPrincipal(original, "").equals(
expected));
assertTrue(SecurityUtil.getServerPrincipal(original, "0.0.0.0").equals(
expected));
}
@Test
public void testGetServerPrincipal() throws IOException {
String service = "hdfs/";
String realm = "@REALM";
String hostname = SecurityUtil.getLocalHostName();
String shouldReplace = service + SecurityUtil.HOSTNAME_PATTERN + realm;
String replaced = service + hostname + realm;
verify(shouldReplace, hostname, replaced);
String shouldNotReplace = service + SecurityUtil.HOSTNAME_PATTERN + "NAME"
+ realm;
verify(shouldNotReplace, hostname, shouldNotReplace);
verify(shouldNotReplace, shouldNotReplace, shouldNotReplace);
}
} }