You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by dd...@apache.org on 2010/07/20 02:46:19 UTC

svn commit: r965696 - in /hadoop/common/trunk: ./ src/java/org/apache/hadoop/ipc/ src/java/org/apache/hadoop/security/ src/java/org/apache/hadoop/security/authorize/ src/java/org/apache/hadoop/security/token/delegation/ src/test/core/org/apache/hadoop/...

Author: ddas
Date: Tue Jul 20 00:46:19 2010
New Revision: 965696

URL: http://svn.apache.org/viewvc?rev=965696&view=rev
Log:
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.

Modified:
    hadoop/common/trunk/CHANGES.txt
    hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Client.java
    hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/SecurityUtil.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/UserGroupInformation.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/authorize/ServiceAuthorizationManager.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenIdentifier.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java
    hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java
    hadoop/common/trunk/src/test/core/org/apache/hadoop/security/TestSecurityUtil.java

Modified: hadoop/common/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/CHANGES.txt?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/CHANGES.txt (original)
+++ hadoop/common/trunk/CHANGES.txt Tue Jul 20 00:46:19 2010
@@ -74,6 +74,12 @@ Trunk (unreleased changes)
     HADOOP-6905. add buildDTServiceName method to SecurityUtil 
     (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
 
   BUG FIXES

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Client.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Client.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Client.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Client.java Tue Jul 20 00:46:19 2010
@@ -54,6 +54,7 @@ import org.apache.hadoop.net.NetUtils;
 import org.apache.hadoop.security.KerberosInfo;
 import org.apache.hadoop.security.SaslRpcClient;
 import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.apache.hadoop.security.SecurityUtil;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.security.token.Token;
 import org.apache.hadoop.security.token.TokenIdentifier;
@@ -254,13 +255,15 @@ public class Client {
         KerberosInfo krbInfo = protocol.getAnnotation(KerberosInfo.class);
         if (krbInfo != null) {
           String serverKey = krbInfo.serverPrincipal();
-          if (serverKey != null) {
-            if(LOG.isDebugEnabled()) {
-              LOG.info("server principal key for protocol="
-                  + protocol.getCanonicalName() + " is " + serverKey + 
-                  " and val =" + conf.get(serverKey));
-            }
-            serverPrincipal = conf.get(serverKey);
+          if (serverKey == null) {
+            throw new IOException(
+                "Can't obtain server Kerberos config key from KerberosInfo");
+          }
+          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);
           }
         }
       }

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java Tue Jul 20 00:46:19 2010
@@ -827,6 +827,7 @@ public abstract class Server {
     // Cache the remote host & port info so that even if the socket is 
     // disconnected, we can say where it used to connect to.
     private String hostAddress;
+    private String hostName;
     private int remotePort;
     
     ConnectionHeader header = new ConnectionHeader();
@@ -869,6 +870,7 @@ public abstract class Server {
         this.hostAddress = "*Unknown*";
       } else {
         this.hostAddress = addr.getHostAddress();
+        this.hostName = addr.getCanonicalHostName();
       }
       this.remotePort = socket.getPort();
       this.responseQueue = new LinkedList<Call>();
@@ -891,6 +893,10 @@ public abstract class Server {
       return hostAddress;
     }
 
+    public String getHostName() {
+      return hostName;
+    }
+    
     public void setLastContact(long lastContact) {
       this.lastContact = lastContact;
     }
@@ -1296,7 +1302,7 @@ public abstract class Server {
             && (authMethod != AuthMethod.DIGEST)) {
           ProxyUsers.authorize(user, this.getHostAddress(), conf);
         }
-        authorize(user, header);
+        authorize(user, header, getHostName());
         if (LOG.isDebugEnabled()) {
           LOG.debug("Successfully authorized " + header);
         }
@@ -1626,10 +1632,12 @@ public abstract class Server {
    * 
    * @param user client user
    * @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
    */
   public void authorize(UserGroupInformation user, 
-                        ConnectionHeader connection
+                        ConnectionHeader connection,
+                        String hostname
                         ) throws AuthorizationException {
     if (authorize) {
       Class<?> protocol = null;
@@ -1639,7 +1647,7 @@ public abstract class Server {
         throw new AuthorizationException("Unknown protocol: " + 
                                          connection.getProtocol());
       }
-      ServiceAuthorizationManager.authorize(user, protocol, getConf());
+      ServiceAuthorizationManager.authorize(user, protocol, getConf(), hostname);
     }
   }
   

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/SecurityUtil.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/SecurityUtil.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/SecurityUtil.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/SecurityUtil.java Tue Jul 20 00:46:19 2010
@@ -17,8 +17,10 @@
 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;
 
@@ -29,6 +31,8 @@ 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;
@@ -38,7 +42,8 @@ import sun.security.krb5.PrincipalName;
 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
 @InterfaceStability.Evolving
 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
@@ -49,9 +54,13 @@ public class SecurityUtil {
    *           if TGT can't be found
    */
   private static KerberosTicket getTgtFromSubject() throws IOException {
-    Set<KerberosTicket> tickets = Subject.getSubject(
-        AccessController.getContext()).getPrivateCredentials(
-        KerberosTicket.class);
+    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;
@@ -90,7 +99,8 @@ public class SecurityUtil {
       return;
     
     String serviceName = "host/" + remoteHost.getHost();
-    LOG.debug("Fetching service ticket for host at: " + serviceName);
+    if (LOG.isDebugEnabled())
+      LOG.debug("Fetching service ticket for host at: " + serviceName);
     Credentials serviceCred = null;
     try {
       PrincipalName principal = new PrincipalName(serviceName,
@@ -98,7 +108,7 @@ public class SecurityUtil {
       serviceCred = Credentials.acquireServiceCreds(principal
           .toString(), Krb5Util.ticketToCreds(getTgtFromSubject()));
     } catch (Exception e) {
-      throw new IOException("Invalid service principal name: "
+      throw new IOException("Can't get service ticket for: "
           + serviceName, e);
     }
     if (serviceCred == null) {
@@ -107,6 +117,91 @@ public class SecurityUtil {
     Subject.getSubject(AccessController.getContext()).getPrivateCredentials()
         .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

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/UserGroupInformation.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/UserGroupInformation.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/UserGroupInformation.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/UserGroupInformation.java Tue Jul 20 00:46:19 2010
@@ -437,6 +437,8 @@ public class UserGroupInformation {
       throw new IOException("Login failure for " + user + " from keytab " + 
                             path, le);
     }
+    LOG.info("Login successful for user " + keytabPrincipal
+        + " using keytab file " + keytabFile);
   }
   
   /**

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/authorize/ServiceAuthorizationManager.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/authorize/ServiceAuthorizationManager.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/authorize/ServiceAuthorizationManager.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/authorize/ServiceAuthorizationManager.java Tue Jul 20 00:46:19 2010
@@ -28,6 +28,7 @@ import org.apache.hadoop.classification.
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.CommonConfigurationKeys;
 import org.apache.hadoop.security.KerberosInfo;
+import org.apache.hadoop.security.SecurityUtil;
 import org.apache.hadoop.security.KerberosName;
 import org.apache.hadoop.security.UserGroupInformation;
 
@@ -68,11 +69,14 @@ public class ServiceAuthorizationManager
    * 
    * @param user user accessing the service 
    * @param protocol service being accessed
+   * @param conf configuration to use
+   * @param hostname fully qualified domain name of the client
    * @throws AuthorizationException on authorization failure
    */
   public static void authorize(UserGroupInformation user, 
                                Class<?> protocol,
-                               Configuration conf
+                               Configuration conf,
+                               String hostname
                                ) throws AuthorizationException {
     AccessControlList acl = protocolToAcl.get(protocol);
     if (acl == null) {
@@ -86,7 +90,19 @@ public class ServiceAuthorizationManager
     if (krbInfo != null) {
       String clientKey = krbInfo.clientPrincipal();
       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

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenIdentifier.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenIdentifier.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenIdentifier.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenIdentifier.java Tue Jul 20 00:46:19 2010
@@ -27,6 +27,7 @@ import java.io.IOException;
 
 import org.apache.hadoop.io.Text;
 import org.apache.hadoop.io.WritableUtils;
+import org.apache.hadoop.security.KerberosName;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.security.token.TokenIdentifier;
 
@@ -57,7 +58,12 @@ extends TokenIdentifier {
     if (renewer == null) {
       this.renewer = new Text();
     } 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) {
       this.realUser = new Text();

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java Tue Jul 20 00:46:19 2010
@@ -35,6 +35,7 @@ import javax.crypto.SecretKey;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 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.SecretManager;
 import org.apache.hadoop.util.Daemon;
@@ -280,8 +281,10 @@ extends AbstractDelegationTokenIdentifie
     }
     String owner = id.getUser().getUserName();
     Text renewer = id.getRenewer();
+    KerberosName cancelerKrbName = new KerberosName(canceller);
+    String cancelerShortName = cancelerKrbName.getShortName();
     if (!canceller.equals(owner)
-        && (renewer == null || "".equals(renewer.toString()) || !canceller
+        && (renewer == null || "".equals(renewer.toString()) || !cancelerShortName
             .equals(renewer.toString()))) {
       throw new AccessControlException(canceller
           + " is not authorized to cancel the token");

Modified: hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java (original)
+++ hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java Tue Jul 20 00:46:19 2010
@@ -48,6 +48,7 @@ import org.apache.hadoop.security.token.
 import org.apache.hadoop.security.SaslInputStream;
 import org.apache.hadoop.security.SaslRpcClient;
 import org.apache.hadoop.security.SaslRpcServer;
+import org.apache.hadoop.security.SecurityUtil;
 import org.apache.hadoop.security.UserGroupInformation;
 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 SERVER_PRINCIPAL_KEY = "test.ipc.server.principal";
+  static final String SERVER_KEYTAB_KEY = "test.ipc.server.keytab";
   private static Configuration conf;
   static {
     conf = new Configuration();
@@ -76,6 +78,7 @@ public class TestSaslRPC {
     ((Log4JLogger) SaslRpcClient.LOG).getLogger().setLevel(Level.ALL);
     ((Log4JLogger) SaslRpcServer.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 {
@@ -248,7 +251,8 @@ public class TestSaslRPC {
   static void testKerberosRpc(String principal, String keytab) throws Exception {
     final Configuration newConf = new Configuration(conf);
     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();
     System.out.println("UGI: " + current);
 
@@ -269,6 +273,7 @@ public class TestSaslRPC {
         RPC.stopProxy(proxy);
       }
     }
+    System.out.println("Test is successful.");
   }
   
   @Test

Modified: hadoop/common/trunk/src/test/core/org/apache/hadoop/security/TestSecurityUtil.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/test/core/org/apache/hadoop/security/TestSecurityUtil.java?rev=965696&r1=965695&r2=965696&view=diff
==============================================================================
--- hadoop/common/trunk/src/test/core/org/apache/hadoop/security/TestSecurityUtil.java (original)
+++ hadoop/common/trunk/src/test/core/org/apache/hadoop/security/TestSecurityUtil.java Tue Jul 20 00:46:19 2010
@@ -17,6 +17,9 @@
 package org.apache.hadoop.security;
 
 import static org.junit.Assert.*;
+
+import java.io.IOException;
+
 import org.junit.Test;
 
 public class TestSecurityUtil {
@@ -32,4 +35,30 @@ public class TestSecurityUtil {
     assertFalse(SecurityUtil.isOriginalTGT("this@is/notright"));
     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);
+  }
 }